text: odoo.define('mail.chatter_tests', function (require) {
  "use strict";
  
  var mailTestUtils = require('mail.testUtils');
  
  var core = require('web.core');
  var FormView = require('web.FormView');
  var KanbanView = require('web.KanbanView');
  var ListView = require('web.ListView');
  var testUtils = require('web.test_utils');
  var concurrency = require('web.concurrency');
  
  var createView = testUtils.createView;
  
  var Activity = require('mail.Activity');
  var _t = core._t;
  
  QUnit.module('mail', {}, function () {
  
  QUnit.module('Chatter', {
      beforeEach: function () {
          // patch _.debounce and _.throttle to be fast and synchronous
          this.underscoreDebounce = _.debounce;
          this.underscoreThrottle = _.throttle;
          _.debounce = _.identity;
          _.throttle = _.identity;
  
          this.services = mailTestUtils.getMailServices();
          this.data = {
              'res.partner': {
                  fields: {
                      im_status: {
                          string: "im_status",
                          type: "char",
                      }
                  },
                  records: [{
                      id: 1,
                      im_status: 'online',
                  }]
              },
              partner: {
                  fields: {
                      display_name: { string: "Displayed name", type: "char" },
                      foo: {string: "Foo", type: "char", default: "My little Foo Value"},
                      message_follower_ids: {
                          string: "Followers",
                          type: "one2many",
                          relation: 'mail.followers',
                          relation_field: "res_id",
                      },
                      message_ids: {
                          string: "messages",
                          type: "one2many",
                          relation: 'mail.message',
                          relation_field: "res_id",
                      },
                      activity_ids: {
                          string: 'Activities',
                          type: 'one2many',
                          relation: 'mail.activity',
                          relation_field: 'res_id',
                      },
                      activity_exception_decoration: {
                          string: 'Decoration',
                          type: 'selection',
                          selection: [['warning', 'Alert'], ['danger', 'Error']],
                      },
                      activity_exception_icon: {
                          string: 'icon',
                          type: 'char',
                      },
                      activity_state: {
                          string: 'State',
                          type: 'selection',
                          selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']],
                      },
                      message_attachment_count: {
                          string: 'Attachment count',
                          type: 'integer',
                      },
                  },
                  records: [{
                      id: 2,
                      message_attachment_count: 3,
                      display_name: "first partner",
                      foo: "HELLO",
                      message_follower_ids: [],
                      message_ids: [],
                      activity_ids: [],
                  }]
              },
              'mail.activity': {
                  fields: {
                      activity_type_id: { string: "Activity type", type: "many2one", relation: "mail.activity.type" },
                      create_uid: { string: "Created By", type: "many2one", relation: 'partner' },
                      can_write: { string: "Can write", type: "boolean" },
                      display_name: { string: "Display name", type: "char" },
                      date_deadline: { string: "Due Date", type: "date" },
                      user_id: { string: "Assigned to", type: "many2one", relation: 'partner' },
                      state: {
                          string: 'State',
                          type: 'selection',
                          selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']],
                      },
                      activity_category: {
                          string: 'Category',
                          type: 'selection',
                          selection: [['default', 'Other'], ['upload_file', 'Upload File']],
                      },
                      note : { string: "Note", type: "char" },
                  },
              },
              'mail.activity.type': {
                  fields: {
                      name: { string: "Name", type: "char" },
                      category: {
                          string: 'Category',
                          type: 'selection',
                          selection: [['default', 'Other'], ['upload_file', 'Upload File']],
                      },
                      decoration_type: { string: "Decoration Type", type: "selection", selection: [['warning', 'Alert'], ['danger', 'Error']]},
                      icon: {string: 'icon', type:"char"},
                  },
                  records: [
                      { id: 1, name: "Type 1" },
                      { id: 2, name: "Type 2" },
                      { id: 3, name: "Type 3", category: 'upload_file' },
                      { id: 4, name: "Exception", decoration_type: "warning", icon: "fa-warning"}
                  ],
              },
              'mail.message': {
                  fields: {
                      attachment_ids: {
                          string: "Attachments",
                          type: 'many2many',
                          relation: 'ir.attachment',
                          default: [],
                      },
                      author_id: {
                          string: "Author",
                          relation: 'res.partner',
                      },
                      body: {
                          string: "Contents",
                          type: 'html',
                      },
                      date: {
                          string: "Date",
                          type: 'datetime',
                      },
                      is_note: {
                          string: "Note",
                          type: 'boolean',
                      },
                      is_discussion: {
                          string: "Discussion",
                          type: 'boolean',
                      },
                      is_notification: {
                          string: "Notification",
                          type: 'boolean',
                      },
                      is_starred: {
                          string: "Starred",
                          type: 'boolean',
                      },
                      model: {
                          string: "Related Document Model",
                          type: 'char',
                      },
                      res_id: {
                          string: "Related Document ID",
                          type: 'integer',
                      }
                  },
                  records: [],
              },
              'ir.attachment': {
                  fields:{
                      name:{type:'char', string:"attachment name", required:true},
                      res_model:{type:'char', string:"res model"},
                      res_id:{type:'integer', string:"res id"},
                      url:{type:'char', string:'url'},
                      type:{ type:'selection', selection:[['url',"URL"],['binary',"BINARY"]]},
                      mimetype:{type:'char', string:"mimetype"},
                  },
                  records:[
                      {id:1, type:'url', mimetype:'image/png', name:'filename.jpg',
                       res_id: 7, res_model: 'partner'},
                      {id:2, type:'binary', mimetype:"application/x-msdos-program",
                       name:"file2.txt", res_id: 7, res_model: 'partner'},
                      {id:3, type:'binary', mimetype:"application/x-msdos-program",
                       name:"file3.txt", res_id: 5, res_model: 'partner'},
                  ],
              },
          };
      },
      afterEach: function () {
          // unpatch _.debounce and _.throttle
          _.debounce = this.underscoreDebounce;
          _.throttle = this.underscoreThrottle;
      }
  });
  
  QUnit.test('messaging becomes ready', async function (assert) {
      assert.expect(2);
  
      const initMessagingProm = testUtils.makeTestPromise();
      const form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: `<form string="Partners">
                  <sheet>
                      <field name="foo"/>
                  </sheet>
                  <div class="oe_chatter">
                      <field name="message_ids" widget="mail_thread"/>
                  </div>
              </form>`,
          async mockRPC(route, args) {
              const _super = this._super.bind(this, route, args); // limitation on class.js with async/await
              if (route === '/mail/init_messaging') {
                  await initMessagingProm;
              }
              return _super();
          },
          res_id: 2,
      });
      await testUtils.nextTick();
      assert.containsOnce(
          form,
          '.o_mail_thread_loading',
          "thread should be loading when messaging is not yet ready");
  
      // simulate messaging becoming ready
      initMessagingProm.resolve();
      await testUtils.nextTick();
      assert.containsNone(
          form,
          '.o_mail_thread_loading',
          "thread should no longer be loading when messaging becomes ready");
  
      form.destroy();
  });
  
  QUnit.test('basic rendering', async function (assert) {
      assert.expect(9);
  
      var count = 0;
      var unwanted_read_count = 0;
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          mockRPC: function (route, args) {
              if (route === '/web/dataset/call_kw/mail.followers/read') {
                  unwanted_read_count++;
              }
              if (route === '/mail/read_followers') {
                  count++;
                  return Promise.resolve({
                      followers: [],
                      subtypes: [],
                  });
              }
              return this._super(route, args);
          },
          res_id: 2,
      });
      assert.containsOnce(form, '.o_mail_activity', "there should be an activity widget");
      assert.containsOnce(form, '.o_chatter_topbar .o_chatter_button_schedule_activity',
      "there should be a 'Schedule an activity' button in the chatter's topbar");
      assert.containsOnce(form, '.o_chatter_topbar .o_followers',
      "there should be a followers widget, moved inside the chatter's topbar");
      assert.containsOnce(form, '.o_chatter', "there should be a chatter widget");
      assert.containsOnce(form, '.o_mail_thread');
      assert.containsOnce(form, '.o_chatter_button_attachment', "should have one attachment button");
      assert.containsNone(form, '.o_chatter_topbar .o_chatter_button_log_note',
          "log note button should not be available");
  
      await testUtils.form.clickEdit(form);
      assert.strictEqual(count, 0, "should have done no read_followers rpc as there are no followers");
      assert.strictEqual(unwanted_read_count, 0, "followers should only be fetched with read_followers route");
      form.destroy();
  });
  
  QUnit.test('basic rendering: message_attachment_count can be in view standalone', async function (assert) {
      assert.expect(1);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<group>' +
                          '<field name="message_attachment_count" string="I\'m here"/>' +
                      '</group>' +
                  '</sheet>' +
              '</form>',
          res_id: 2,
      });
  
      assert.strictEqual(form.$('.o_form_label').text(), "I'm here",
          "The field message_attachment_count must be present according to the view's specs");
  
      form.destroy();
  });
  
  QUnit.test('basic rendering: message_attachment_count can be in view with chatter', async function (assert) {
      assert.expect(1);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<group>' +
                          '<field name="message_attachment_count" string="I\'m here"/>' +
                      '</group>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
      });
  
      assert.strictEqual(form.$('.o_form_label').text(), "I'm here",
          "The field message_attachment_count must be present according to the view's specs");
  
      form.destroy();
  });
  
  QUnit.test('Activity Done keep feedback on blur', async function (assert) {
      assert.expect(3);
      var done = assert.async();
  
      this.data['mail.activity'].records = [
          {activity_type_id: 1, id: 1, can_write: true, user_id: 2, state: 'today', note: 'But I\'m talkin\' about Shaft'},
      ];
      this.data.partner.records[0].activity_ids = [1];
  
      var shownDef = testUtils.makeTestPromise();
      var hiddenDef = testUtils.makeTestPromise();
      testUtils.mock.patch(Activity, {
          _bindPopoverFocusout: function () {
              this._super.apply(this, arguments);
              shownDef.resolve();
          },
      });
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          res_id: 2,
          arch:'<form string="Partners">' +
                  '<div class="oe_chatter">' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
      });
      // sanity checks
      var $activityEl = form.$('.o_mail_activity[name=activity_ids]');
      assert.strictEqual($activityEl.find('.o_thread_message').length, 1,
          'There should be one activity');
      assert.strictEqual($activityEl.find('.o_thread_message .o_thread_message_note').text().trim(),
          'But I\'m talkin\' about Shaft', 'The activity should have the right note');
  
      var $popoverEl = $activityEl.find('.o_thread_message_tools .o_mark_as_done');
      $popoverEl.on('hidden.bs.popover', hiddenDef.resolve.bind(hiddenDef));
  
      // open popover
      await testUtils.dom.click($popoverEl);
  
      shownDef.then(function () {
          // write a feedback and focusout
          var $feedbackPopover = $($popoverEl.data('bs.popover').tip);
          $feedbackPopover.find('#activity_feedback').val('John Shaft').focusout();
  
          hiddenDef.then(async function () {
              shownDef = testUtils.makeTestPromise();
  
              // re-open popover
              await testUtils.dom.click($popoverEl);
  
              shownDef.then(function () {
                  var $feedbackPopover = $($popoverEl.data('bs.popover').tip);
                  assert.strictEqual($feedbackPopover.find('#activity_feedback').val(), 'John Shaft',
                      "feedback should have been kept");
  
                  form.destroy();
                  testUtils.mock.unpatch(Activity);
                  done();
              });
          });
      });
  });
  
  QUnit.test('Activity Done by uploading a file', async function (assert) {
      assert.expect(4);
  
      // simulate (shortcut) the upload and trigger the event when the
      // attachment is created serverside.
      testUtils.mock.patch(Activity, {
          _onMarkActivityDoneUploadFile: function (ev) {
              var $markDoneBtn = $(ev.currentTarget);
              var fileuploadID = $markDoneBtn.data('fileupload-id');
  
              $(window).trigger(fileuploadID, [{
                  id:3,
                  type:'binary',
                  mimetype:"application/x-msdos-program",
                  name:"file2.txt",
                  res_id: 5,
                  res_model: 'partner'
              }]);
          },
      });
  
      // create the activity
      this.data['mail.activity'].records = [{
          activity_type_id: 3,
          id: 1,
          can_write: true,
          user_id: 2,
          state: 'today',
          note: "But I'm talkin' about Shaft",
          activity_category: 'upload_file'
      }];
      this.data.partner.records[0].activity_ids = [1];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          res_id: 2,
          arch:'<form string="Partners">' +
                  '<div class="oe_chatter">' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          mockRPC: function (route, args) {
              if (args.method === 'action_feedback') {
                  var current_ids = this.data.partner.records[0].activity_ids;
                  var done_ids = args.args[0];
                  this.data.partner.records[0].activity_ids = _.difference(current_ids, done_ids);
                  this.data.partner.records[0].activity_state = false;
                  return Promise.resolve();
              }
              return this._super.apply(this, arguments);
          },
      });
  
      var $activity = form.$('.o_mail_activity[name=activity_ids]');
      assert.containsOnce($activity, '.o_thread_message', 'There should be one activity');
      assert.containsOnce($activity, '.o_hidden_input_file', 'There should be one hidden file upload form');
      assert.strictEqual($activity.find('.o_thread_message .o_thread_message_note').text().trim(),
          'But I\'m talkin\' about Shaft', 'The activity should have the right note');
  
      // click on "upload" button
      var $uploadBtn = $activity.find('.o_mark_as_done_upload_file');
      await testUtils.dom.click($uploadBtn);
  
      assert.containsNone($activity, '.o_thread_message', 'The only activity should be marked as done');
  
      testUtils.mock.unpatch(Activity);
      form.destroy();
  });
  
  QUnit.test('attachmentBox basic rendering', async function (assert) {
      assert.expect(19);
      this.data.partner.records.push({
          id: 7,
          display_name: "attachment_test",
      });
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 7,
          mockRPC: function (route, args) {
              var result = this._super.apply(this, arguments);
              if (args.method === 'read' && args.model === 'partner') {
                  return result.then(function (records) {
                      // here we force the attachment_count to 1 (which is correct
                      // actually), so that the attachment button is visible
                      // FIXME: this could be handled by an extension of mockRead
                      records[0].message_attachment_count = 1;
                      return records;
                  });
              }
              return result;
          },
      });
      var $button = form.$('.o_chatter_button_attachment');
      assert.strictEqual($button.length, 1, "should have one attachment button");
      await testUtils.dom.click($button);
      assert.containsOnce(form, '.o_mail_chatter_attachments',
          "attachment widget should exist after a first click on the button");
      assert.containsOnce(form, '.o_attachment_image', "there should be an image preview");
      assert.containsOnce(form, '.o_attachments_previews', "there should be a list of previews");
      assert.containsOnce(form, '.o_attachments_list', "there should be a list of non previewable attachments");
      assert.containsOnce(form, '.o_upload_attachments_button', "there should be an 'Add Attachments' button");
      assert.containsOnce(form, '.o_form_binary_form', "there should be a binary form");
  
      assert.containsOnce(form, 'input[name="model"]', "there should be an model input");
      var $modelInput = form.$('input[name="model"]');
      assert.hasAttrValue($modelInput, 'value', 'partner');
      assert.hasAttrValue($modelInput, 'type', 'hidden');
  
      assert.containsOnce(form, 'input[name="model"]', "there should be an id input");
      var $resIdInput = form.$('input[name="id"]');
      assert.hasAttrValue($resIdInput, 'value', '7');
      assert.hasAttrValue($resIdInput, 'type', 'hidden');
  
      assert.strictEqual(form.$('.o_attachment_title').text(), 'filename.jpg',
          "the image name should be correct");
      // since there are two elements "Download name2"; one "name" and the other "txt" as text content, the following test
      // asserts both at the same time.
      assert.strictEqual(form.$('a[title = "Download file2.txt"]').text().trim(), 'file2.txttxt',
          "the attachment name and the extension display should be correct");
      assert.ok(form.$('.o_attachment_image').css('background-image').indexOf('/web/image/1/160x160/?crop=true') >= 0,
          "the attachment image URL should be correct");
      assert.hasAttrValue(form.$('.o_attachment_download').eq(0), 'href', '/web/content/1?download=true',
          "the download URL of name1 must be correct");
      assert.hasAttrValue(form.$('.o_attachment_download').eq(1), 'href', '/web/content/2?download=true',
          "the download URL of name2 must be correct");
      await testUtils.dom.click($button);
      assert.containsNone(form, '.o_mail_chatter_attachments')
      form.destroy();
  });
  
  QUnit.test('attachmentBox: deletable attachments', async function (assert) {
      assert.expect(1);
      this.data.partner.records.push({
          id: 7,
          display_name: "attachment_test",
      });
  
      const form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: `
              <form string="Partners">
                  <sheet>
                      '<field name="foo"/>
                  </sheet>
                  <div class="oe_chatter">
                      '<field name="message_ids" widget="mail_thread"/>
                  </div>
              </form>`,
          res_id: 7,
      });
      await testUtils.dom.click(form.$('.o_chatter_button_attachment'));
      assert.containsN(
          form,
          '.o_attachment_delete_cross',
          2,
          "Attachments should be deletable inside attachment box");
  
      form.destroy();
  });
  
  QUnit.test('chatter in create mode', async function (assert) {
      assert.expect(9);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (route === "/mail/get_suggested_recipients") {
                  return Promise.resolve({2: []});
              }
              return this._super(route, args);
          },
      });
  
      assert.containsOnce(form, '.o_chatter',
          "chatter should be displayed");
  
      // entering create mode
      await testUtils.form.clickCreate(form);
      assert.hasClass(form.$el.find('.o_form_view'),'o_form_editable',
          "we should be in create mode");
      assert.containsOnce(form, '.o_chatter',
          "chatter should still be displayed in create mode");
  
      // topbar buttons disabled in create mode (e.g. 'send message')
      assert.strictEqual(form.$('.o_chatter_topbar button:not(:disabled)').length, 0,
          "button should be disabled in create mode");
  
      // chatter containing a single message with 'Creating a record...'
      assert.containsOnce(form, '.o_mail_thread',
          "there should be a mail thread");
      assert.containsOnce(form, '.o_thread_message',
          "there should be a single thread message");
      assert.strictEqual(form.$('.o_thread_message_content').text().trim(),
          "Creating a new record...",
          "the content of the message should be 'Creating a new record...'");
  
      // getting out of create mode by saving
      await testUtils.fields.editInput(form.$('.o_field_char'), 'coucou');
      await testUtils.form.clickSave(form);
  
      assert.containsOnce(form, '.o_chatter',
          "chatter should still be displayed after saving from create mode");
  
      // check if chatter buttons still work
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.containsOnce(form, '.o_thread_composer:visible',
          "chatter should be opened");
  
      form.destroy();
  });
  
  QUnit.test('chatter rendering inside the sheet', async function (assert) {
      assert.expect(5);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                      '<notebook>' +
                          '<page>' +
                              '<div class="oe_chatter">' +
                                  '<field name="message_ids" widget="mail_thread"/>' +
                              '</div>' +
                          '</page>' +
                      '</notebook>' +
                  '</sheet>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (route === "/mail/get_suggested_recipients") {
                  return Promise.resolve({2: []});
              }
              return this._super(route, args);
          },
      });
  
      assert.containsOnce(form, '.o_chatter',
          "chatter should be displayed");
  
      await testUtils.form.clickCreate(form);
      assert.hasClass(form.$el.find('.o_form_view'),'o_form_editable',
          "we should be in create mode");
  
      assert.containsOnce(form, '.o_chatter',
          "chatter should be displayed");
  
      await testUtils.fields.editInput(form.$('.o_field_char'), 'coucou');
      await testUtils.form.clickSave(form);
  
      assert.containsOnce(form, '.o_chatter',
          "chatter should be displayed");
  
      // check if chatter buttons still work
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.containsOnce(form, '.o_thread_composer:visible',
          "chatter should be opened");
  
      form.destroy();
  });
  
  QUnit.test('kanban activity widget with no activity', async function (assert) {
      assert.expect(4);
  
      var rpcCount = 0;
      var kanban = await createView({
          View: KanbanView,
          model: 'partner',
          data: this.data,
          arch: '<kanban>' +
                      '<field name="activity_state"/>' +
                      '<templates><t t-name="kanban-box">' +
                          '<div><field name="activity_ids" widget="kanban_activity"/></div>' +
                      '</t></templates>' +
                  '</kanban>',
          mockRPC: function (route, args) {
              rpcCount++;
              return this._super(route, args);
          },
          session: {uid: 2},
      });
  
      var $record = kanban.$('.o_kanban_record').first();
      assert.ok($record.find('.o_mail_activity .o_activity_color_default').length,
          "activity widget should have been rendered correctly");
      assert.strictEqual(rpcCount, 1, '1 RPC (search_read) should have been done');
  
      // click on the activity button
      await testUtils.dom.click($record.find('.o_activity_btn'));
      assert.strictEqual(rpcCount, 1, 'no RPC should have been done as there is no activity');
      assert.strictEqual($record.find('.o_no_activity').length, 1, "should have no activity scheduled");
  
      // fixme: it would be nice to be able to test the scheduling of a new activity, but not
      // possible for now as we can't mock a fields_view_get (required by the do_action)
      kanban.destroy();
  });
  
  QUnit.test('kanban activity widget with an activity', async function (assert) {
      assert.expect(14);
  
      this.data.partner.records[0].activity_ids = [1, 2];
      this.data.partner.records[0].activity_state = 'today';
      this.data.partner.records[0].activity_exception_decoration = 'warning';
      this.data.partner.records[0].activity_exception_icon = 'fa-warning';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 1,
      },{
          id: 2,
          display_name: "An exception activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 4,
      }];
      var rpcCount = 0;
      var kanban = await createView({
          View: KanbanView,
          model: 'partner',
          data: this.data,
          arch: '<kanban>' +
                      '<field name="activity_state"/>' +
                      '<templates><t t-name="kanban-box">' +
                          '<div><field name="activity_ids" widget="kanban_activity"/></div>' +
                      '</t></templates>' +
                  '</kanban>',
          mockRPC: function (route, args) {
              rpcCount++;
              if (route === '/web/dataset/call_kw/mail.activity/action_feedback') {
                  var current_ids = this.data.partner.records[0].activity_ids;
                  var done_ids = args.args[0];
                  this.data.partner.records[0].activity_ids = _.difference(current_ids, done_ids);
                  this.data.partner.records[0].activity_exception_decoration = false;
                  this.data.partner.records[0].activity_state = false;
                  return Promise.resolve();
              }
              return this._super(route, args);
          },
          session: {uid:2},
      });
  
      var $record = kanban.$('.o_kanban_record').first();
      assert.ok($record.find('.o_mail_activity .o_activity_color_today').length,
          "activity widget should have been rendered correctly");
      assert.strictEqual(rpcCount, 1, '1 RPC (search_read) should have been done');
  
      // Check exception activity icon fa-warning
      assert.hasClass($record.find('.o_activity_btn span'), "fa-warning", "should display correct exception activity icon");
      // click on the activity button
      await testUtils.dom.click($record.find('.o_activity_btn'));
      assert.strictEqual(rpcCount, 2, 'a read should have been done to fetch the activity details');
      assert.strictEqual($record.find('.o_activity_title').length, 2, "should have two activity scheduled");
      var label = $record.find('.o_activity_log .o_activity_color_today');
      assert.strictEqual(label.find('strong').text(), "Today", "should display the correct label");
      assert.strictEqual(label.find('.badge-warning').text(), "2", "should display the correct count");
  
      // click on the activity button to close the dropdown
      await testUtils.dom.click($record.find('.o_activity_btn'));
      assert.strictEqual(rpcCount, 2, 'no RPC should be done when closing the dropdown');
  
      // click on the activity button to re-open dropdown
      await testUtils.dom.click($record.find('.o_activity_btn'));
      assert.strictEqual(rpcCount, 3, 'should have reloaded the activities');
  
      // mark exception activity as done
      await testUtils.dom.click($record.find('.o_mark_as_done:eq(1)'));
      await testUtils.dom.click($record.find('.o_activity_popover_done'));
      $record = kanban.$('.o_kanban_record').first(); // the record widget has been reset
  
      // check activity icon replaced with fa-clock
      assert.hasClass($record.find('.o_activity_btn span'), "fa-clock-o", "should display correct activity icon");
  
      // mark activity as done
      await testUtils.dom.click($record.find('.o_mark_as_done'));
      await testUtils.dom.click($record.find('.o_activity_popover_done'));
      $record = kanban.$('.o_kanban_record').first(); // the record widget has been reset
      assert.strictEqual(rpcCount, 8, 'should have done an RPC to mark activity as done, and a read');
      assert.ok($record.find('.o_mail_activity .o_activity_color_default:not(.o_activity_color_today)').length,
          "activity widget should have been updated correctly");
      assert.strictEqual($record.find('.o_mail_activity.show').length, 1,
          "dropdown should remain open when marking an activity as done");
      assert.strictEqual($record.find('.o_no_activity').length, 1, "should have no activity scheduled");
  
      kanban.destroy();
  });
  
  QUnit.test('kanban activity widget popover test', async function (assert) {
      assert.expect(3);
  
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 1,
      }];
      var rpcCount = 0;
      var kanban = await createView({
          View: KanbanView,
          model: 'partner',
          data: this.data,
          arch: '<kanban>' +
                      '<field name="activity_state"/>' +
                      '<templates><t t-name="kanban-box">' +
                          '<div><field name="activity_ids" widget="kanban_activity"/></div>' +
                      '</t></templates>' +
                  '</kanban>',
          mockRPC: function (route, args) {
              if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') {
                  rpcCount++;
  
                  var current_ids = this.data.partner.records[0].activity_ids;
                  var done_ids = args.args[0];
                  this.data.partner.records[0].activity_ids = _.difference(current_ids, done_ids);
                  this.data.partner.records[0].activity_state = false;
                  return Promise.resolve();
              }
              return this._super(route, args);
          },
      });
  
      var $record = kanban.$('.o_kanban_record').first();
  
      await testUtils.dom.click($record.find('.o_activity_btn'));
  
      // Click on button and see popover no RPC call
      await testUtils.dom.click($record.find('.o_mark_as_done'));
      assert.equal(rpcCount, 0, "");
      // Click on discard no RPC call
      await testUtils.dom.click($record.find('.o_activity_popover_discard'));
      assert.equal(rpcCount, 0, "");
      // Click on button and then on done and schedule next
      // RPC call
      await testUtils.dom.click($record.find('.o_activity_popover_done_next'));
      assert.equal(rpcCount, 1, "");
  
      kanban.destroy();
  });
  
  QUnit.test('list activity exception widget with activity', async function (assert) {
      assert.expect(3);
  
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records.push({
          id: 3,
          message_attachment_count: 3,
          display_name: "second partner",
          foo: "Tommy",
          message_follower_ids: [],
          message_ids: [],
          activity_ids: [2],
          activity_exception_decoration: 'warning',
          activity_exception_icon: 'fa-warning',
      });
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 1,
      },{
          id: 2,
          display_name: "An exception activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 4,
      }];
  
      var list = await createView({
          View: ListView,
          model: 'partner',
          data: this.data,
          arch: '<tree>' +
                  '<field name="foo"/>'+
                  '<field name="activity_exception_decoration" widget="activity_exception"/> ' +
              '</tree>',
      });
  
      assert.containsN(list, '.o_data_row', 2, "should have two records");
      assert.doesNotHaveClass(list.$('.o_data_row:eq(0) .o_activity_exception_cell div'), 'fa-warning',
          "there is no any exception activity on record");
      assert.hasClass(list.$('.o_data_row:eq(1) .o_activity_exception_cell div'), 'fa-warning',
          "there is an exception on a record");
  
      list.destroy();
  });
  
  QUnit.test('chatter: post, receive and star messages', async function (assert) {
      assert.expect(26);
  
      this.data.partner.records[0].message_ids = [1];
      this.data['mail.message'].records = [{
          author_id: ["1", "John Doe"],
          body: "A message",
          date: "2016-12-20 09:35:40",
          id: 1,
          is_note: false,
          is_discussion: true,
          is_notification: false,
          is_starred: false,
          model: 'partner',
          res_id: 2,
      }];
  
      var getSuggestionsDef = testUtils.makeTestPromise();
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread" options="{\'display_log_button\': True}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (route === '/mail/get_suggested_recipients') {
                  return Promise.resolve({2: []});
              }
              if (args.method === 'get_mention_suggestions') {
                  getSuggestionsDef.resolve();
                  return Promise.resolve([[{email: "test@odoo.com", id: 1, name: "Test User"}], []]);
              }
              if (args.method === 'message_post') {
                  var lastMessageData = _.max(this.data['mail.message'].records, function (messageData) {
                      return messageData.id;
                  });
                  var messageID = lastMessageData.id + 1;
                  this.data['mail.message'].records.push({
                      attachment_ids: args.kwargs.attachment_ids,
                      author_id: ["42", "Me"],
                      body: args.kwargs.body,
                      date: "2016-12-20 10:35:40",
                      id: messageID,
                      is_note: args.kwargs.subtype === 'mail.mt_note',
                      is_discussion: args.kwargs.subtype === 'mail.mt_comment',
                      is_notification: false,
                      is_starred: false,
                      model: 'partner',
                      res_id: 2,
                  });
                  return Promise.resolve(messageID);
              }
              if (args.method === 'toggle_message_starred') {
                  assert.ok(_.contains(args.args[0], 2),
                      "toggle_star_status should have been triggered for message 2 (twice)");
                  var messageData = _.findWhere(
                      this.data['mail.message'].records,
                      { id: args.args[0][0] }
                  );
                  messageData.is_starred = !messageData.is_starred;
                  // simulate notification received by mail_service from longpoll
                  var data = {
                      info: false,
                      message_ids: [messageData.id],
                      starred: messageData.is_starred,
                      type: 'toggle_star',
                  };
                  var notification = [[false, 'res.partner'], data];
                  form.call('bus_service', 'trigger', 'notification', [notification]);
                  return Promise.resolve();
              }
              return this._super(route, args);
          },
          session: {},
      });
  
      assert.ok(form.$('.o_chatter_topbar .o_chatter_button_log_note').length,
          "log note button should be available");
      assert.containsOnce(form, '.o_thread_message', "thread should contain one message");
      assert.ok(form.$('.o_thread_message:first().o_mail_discussion').length,
          "the message should be a discussion");
      assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('A message') >= 0,
          "the message's body should be correct");
      assert.ok(form.$('.o_thread_message:first() .o_mail_info').text().indexOf('John Doe') >= 0,
          "the message's author should be correct");
  
      // send a message
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.isVisible($('.oe_chatter .o_thread_composer'), "chatter should be opened");
      form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
      assert.isNotVisible($('.oe_chatter .o_thread_composer'), "chatter should be closed");
      assert.containsN(form, '.o_thread_message', 2, "thread should contain two messages");
      assert.ok(form.$('.o_thread_message:first().o_mail_discussion').length,
          "the last message should be a discussion");
      assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('My first message') >= 0,
          "the message's body should be correct");
      assert.ok(form.$('.o_thread_message:first() .o_mail_info').text().indexOf('Me') >= 0,
          "the message's author should be correct");
  
      // log a note
      await testUtils.dom.click(form.$('.o_chatter_button_log_note'));
      assert.isVisible($('.oe_chatter .o_thread_composer'), "chatter should be opened");
      form.$('.oe_chatter .o_composer_text_field:first()').val("My first note");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
      assert.isNotVisible($('.oe_chatter .o_thread_composer'), "chatter should be closed");
      assert.containsN(form, '.o_thread_message', 3, "thread should contain three messages");
      assert.ok(!form.$('.o_thread_message:first().o_mail_discussion').length,
          "the last message should not be a discussion");
      assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('My first note') >= 0,
          "the message's body should be correct");
      assert.ok(form.$('.o_thread_message:first() .o_mail_info').text().indexOf('Me') >= 0,
          "the message's author should be correct");
  
      // star message 2
      assert.ok(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star.fa-star-o').length,
          "message 2 should not be starred");
      await testUtils.dom.click(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star'));
      assert.ok(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star.fa-star').length,
          "message 2 should be starred");
  
      // unstar message 2
      await testUtils.dom.click(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star'));
      assert.ok(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star.fa-star-o').length,
          "message 2 should not be starred");
  
      // very basic test of mention
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      var $input = form.$('.oe_chatter .o_composer_text_field:first()');
      $input.val('@');
      // the cursor position must be set for the mention manager to detect that we are mentionning
      $input[0].selectionStart = 1;
      $input[0].selectionEnd = 1;
      $input.trigger('keyup');
  
      await testUtils.nextTick();
      await getSuggestionsDef;
      await testUtils.nextTick();
      assert.containsOnce(form, '.o_mention_proposition:visible',
          "there should be one mention suggestion");
      assert.strictEqual(form.$('.o_mention_proposition').data('id'), 1,
          "suggestion's id should be correct");
      assert.strictEqual(form.$('.o_mention_proposition .o_mention_name').text(), 'Test User',
          "suggestion should be displayed correctly");
      assert.strictEqual(form.$('.o_mention_proposition .o_mention_info').text(), '(test@odoo.com)',
          "suggestion should be displayed correctly");
      //cleanup
      form.destroy();
  });
  
  QUnit.test('chatter: post a message disable the send button', async function(assert) {
      assert.expect(3);
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {},
          mockRPC: function (route, args) {
              if (route === '/mail/get_suggested_recipients') {
                  return Promise.resolve({2: []});
              }
              if (args.method === 'message_post') {
                  assert.ok(form.$('.o_composer_button_send').prop("disabled"),
                      "Send button should be disabled when a message is being sent");
                  return Promise.resolve(57923);
              }
              if (args.method === 'message_format') {
                  return Promise.resolve([{
                      author_id: ["42", "Me"],
                      model: 'partner',
                  }]);
              }
              return this._super(route, args);
          },
      });
  
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
          "Send button should be enabled when posting a message");
      form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
          "Send button should be enabled when posting another message");
      form.destroy();
  });
  
  QUnit.test('chatter: post message failure keep message', async function(assert) {
      assert.expect(4);
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {},
          mockRPC: function (route, args) {
              if (route === '/mail/get_suggested_recipients') {
                  return Promise.resolve({2: []});
              }
              if (args.method === 'message_post') {
                  assert.ok(form.$('.o_composer_button_send').prop("disabled"),
                      "Send button should be disabled when a message is being sent");
                  // simulate failure
                  return Promise.reject();
  
              }
              if (args.method === 'message_format') {
                  return Promise.resolve([]);
              }
              return this._super(route, args);
          },
      });
  
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
          "Send button should be enabled initially");
      await testUtils.fields.editInput(form.$('.oe_chatter .o_composer_text_field:first()'), "My first message");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
      assert.strictEqual(form.$('.o_composer_text_field').val(), "My first message",
          "Should keep unsent message in the composer on failure");
      assert.notOk(form.$('.o_composer_button_send').prop('disabled'),
          "Send button should be re-enabled on message post failure");
      form.destroy();
  });
  
  QUnit.test('chatter: receive notif when document is open', async function (assert) {
      assert.expect(2);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {
              partner_id: 3,
          },
      });
  
      var thread = form.call('mail_service', 'getDocumentThread', 'partner', 2);
      assert.strictEqual(thread.getUnreadCounter(), 0,
          "document thread should have no unread messages initially");
  
      // simulate receiving a needaction message on this document thread
      var needactionMessageData = {
          id: 5,
          author_id: [42, "Someone"],
          body: 'important message',
          channel_ids: [],
          res_id: 2,
          model: 'partner',
          needaction: true,
          needaction_partner_ids: [3],
      };
      this.data['mail.message'].records.push(needactionMessageData);
      var notification = [[false, 'mail.channel', 1], needactionMessageData];
      form.call('bus_service', 'trigger', 'notification', [notification]);
      await testUtils.nextMicrotaskTick();
      assert.strictEqual(thread.getUnreadCounter(), 1,
          "document thread should now have one unread message");
  
      // do not destroy form too early. wait rendering to avoid race condition in service call.
      await testUtils.nextTick();
      form.destroy();
  });
  
  QUnit.test('chatter: access document with some notifs', async function (assert) {
      assert.expect(3);
  
      // simulate received needaction message on this document thread
      var needactionMessageData = {
          id: 5,
          author_id: [42, "Someone"],
          body: 'important message',
          channel_ids: [],
          res_id: 2,
          model: 'partner',
          needaction: true,
          needaction_partner_ids: [3],
      };
      this.data['mail.message'].records.push(needactionMessageData);
      this.data['partner'].records[0].message_ids = [5];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {
              partner_id: 3,
          },
          mockRPC: function (route, args) {
              if (args.method === 'set_message_done') {
                  assert.step('set_message_done');
              }
              return this._super.apply(this, arguments);
          },
      });
  
      assert.verifySteps(['set_message_done']);
  
      var thread = form.call('mail_service', 'getDocumentThread', 'partner', 2);
      assert.strictEqual(thread.getUnreadCounter(), 0,
          "document thread should have no unread messages (marked as read)");
  
      form.destroy();
  });
  
  QUnit.test('chatter: new message notification from document', async function (assert) {
      // when receiving new messages from a document that is open, it should
      // not mark the messages are read
      assert.expect(4);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {
              partner_id: 3,
          },
          mockRPC: function (route, args) {
              if (args.method === 'set_message_done') {
                  throw new Error("should not mark message as read");
              }
              return this._super.apply(this, arguments);
          },
      });
  
      var thread = form.call('mail_service', 'getDocumentThread', 'partner', 2);
      assert.strictEqual(
          thread.getUnreadCounter(),
          0,
          "document thread should have no unread messages initially");
      assert.containsNone(
          form,
          '.o_thread_message',
          "should have no message in the chatter");
  
      // Simulate received needaction`` message on this document
      var message = {
          author_id: [1, "Me"],
          body: '<p>test</p>',
          channel_ids: [],
          id: 2,
          model: 'partner',
          needaction_partner_ids: [3],
          res_id: 2,
      };
      const notifications = [ [['myDB', 'ir.needaction'], message] ];
      form.call('bus_service', 'trigger', 'notification', notifications);
      await testUtils.nextTick();
      assert.strictEqual(
          thread.getUnreadCounter(),
          1,
          "document thread should have one unread message (not marked as read)");
      assert.containsOnce(
          form,
          '.o_thread_message',
          "should have one message in the chatter");
  
      form.destroy();
  });
  
  QUnit.test('chatter: post a message and switch in edit mode', async function (assert) {
      assert.expect(5);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread" options="{\'display_log_button\': True}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {},
          mockRPC: function (route, args) {
              if (route === '/mail/get_suggested_recipients') {
                  return Promise.resolve({2: []});
              }
              if (args.method === 'message_post') {
                  this.data['mail.message'].records.push({
                      attachment_ids: args.kwargs.attachment_ids,
                      author_id: ["42", "Me"],
                      body: args.kwargs.body,
                      date: "2016-12-20 10:35:40",
                      id: 42,
                      is_note: args.kwargs.subtype === 'mail.mt_note',
                      is_discussion: args.kwargs.subtype === 'mail.mt_comment',
                      is_starred: false,
                      model: 'partner',
                      res_id: 2,
                  });
                  return Promise.resolve(42);
              }
  
              return this._super(route, args);
          },
      });
  
      assert.containsNone(form, '.o_thread_message', "thread should not contain messages");
  
      // send a message
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
      assert.containsOnce(form, '.o_thread_message', "thread should contain a message");
      assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('My first message') >= 0,
          "the message's body should be correct");
  
      // switch in edit mode
      await testUtils.form.clickEdit(form);
      assert.containsOnce(form, '.o_thread_message', "thread should contain a message");
      assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('My first message') >= 0,
          "the message's body should be correct");
  
      form.destroy();
  });
  
  QUnit.test('chatter: discard changes on message post with post_refresh "always"', async function (assert) {
      // After posting a message that always reloads the record, if the record
      // is dirty (= has some unsaved changes), we should warn the user that
      // these changes will be lost if he proceeds.
      assert.expect(2);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"' +
                          ' options="{\'display_log_button\': True, \'post_refresh\': \'always\'}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {},
          mockRPC: function (route, args) {
              if (route === "/mail/get_suggested_recipients") {
                  return Promise.resolve({2: []});
              }
              return this._super(route, args);
          },
          viewOptions: {
              mode: 'edit',
          },
      });
  
      // Make record dirty
      await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu');
  
      // Send a message
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
  
      var $modal = $('.modal-dialog');
      assert.strictEqual($modal.length, 1, "should have a modal opened");
      assert.strictEqual($modal.find('.modal-body').text(),
          "The record has been modified, your changes will be discarded. Do you want to proceed?",
          "should warn the user that any unsaved changes will be lost");
  
      form.destroy();
  });
  
  QUnit.test('chatter: discard changes on message post without post_refresh', async function (assert) {
      // After posting a message, if the record is dirty and there are no
      // post_refresh rule, it will not discard the changes on the record.
      assert.expect(2);
  
      var hasDiscardChanges = false; // set if `discard_changes` has been triggered up
      var messages = [];
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"' +
                          ' options="{\'display_log_button\': True}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {},
          mockRPC: function (route, args) {
              if (route === "/mail/get_suggested_recipients") {
                  return Promise.resolve({2: []});
              }
              if (args.method === 'message_format') {
                  var requested_msgs = _.filter(messages, function (msg) {
                      return _.contains(args.args[0], msg.id);
                  });
                  return Promise.resolve(requested_msgs);
              }
              if (args.method === 'message_post') {
                  messages.push({
                      attachment_ids: [],
                      author_id: ["42", "Me"],
                      body: args.kwargs.body,
                      date: moment().format('YYYY-MM-DD HH:MM:SS'), // now
                      displayed_author: "Me",
                      id: 42,
                      is_note: args.kwargs.subtype === 'mail.mt_note',
                      is_starred: false,
                      model: 'partner',
                      res_id: 2,
                  });
                  return Promise.resolve(42);
              }
              return this._super(route, args);
          },
          intercepts: {
              discard_changes: function () {
                  hasDiscardChanges = true; // should not do that
              },
          },
          viewOptions: {
              mode: 'edit',
          },
      });
  
      // Make record dirty
      await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu');
  
      // Send a message
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
  
      var $modal = $('.modal-dialog');
      assert.strictEqual($modal.length, 0, "should have no modal opened");
      assert.notOk(hasDiscardChanges);
  
      form.destroy();
  });
  
  QUnit.test('chatter: discard changes on message post with post_refresh "recipients"', async function (assert) {
      // After posting a message with mentions, the record will be reloaded,
      // as the rpc `message_post` may make changes on some fields of the record.
      // If the record is dirty (= has some unsaved changes), we should warn the
      // user that these changes will be lost if he proceeds.
      assert.expect(2);
  
      var getSuggestionsDef = testUtils.makeTestPromise();
  
      var messages = [];
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"' +
                          ' options="{\'display_log_button\': True, \'post_refresh\': \'recipients\'}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {},
          mockRPC: function (route, args) {
              if (route === "/mail/get_suggested_recipients") {
                  return Promise.resolve({2: [[42, "Me"]]});
              }
              if (args.method === 'get_mention_suggestions') {
                  getSuggestionsDef.resolve();
                  return Promise.resolve([[{email: "me@odoo.com", id: 42, name: "Me"}], []]);
              }
              if (args.method === 'message_format') {
                  var requested_msgs = _.filter(messages, function (msg) {
                      return _.contains(args.args[0], msg.id);
                  });
                  return Promise.resolve(requested_msgs);
              }
              if (args.method === 'message_post') {
                  messages.push({
                      attachment_ids: [],
                      author_id: ["42", "Me"],
                      body: args.kwargs.body,
                      date: moment().format('YYYY-MM-DD HH:MM:SS'), // now
                      displayed_author: "Me",
                      id: 42,
                      is_note: args.kwargs.subtype === 'mail.mt_note',
                      is_starred: false,
                      model: 'partner',
                      res_id: 2,
                  });
                  return Promise.resolve(42);
              }
              return this._super(route, args);
          },
          viewOptions: {
              mode: 'edit',
          },
      });
  
      // Make record dirty
      await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu');
  
      // create a new message
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
  
      // Add a user as mention
      await testUtils.fields.editInput(form.$('.oe_chatter .o_composer_text_field:first()'), "@");
  
      var $input = form.$('.oe_chatter .o_composer_text_field:first()');
      $input.val('@');
      // the cursor position must be set for the mention manager to detect that we are mentionning
      $input[0].selectionStart = 1;
      $input[0].selectionEnd = 1;
      $input.trigger('keyup');
      await getSuggestionsDef;
      await testUtils.nextTick();
      // click on mention
      $input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER}));
      await testUtils.nextTick();
  
      // untick recipient as follower (prompts a res.partner form otherwise)
      form.$('input[type="checkbox"]').prop('checked', false);
      await testUtils.nextTick();
      // send message
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
  
      var $modal = $('.modal-dialog');
      assert.strictEqual($modal.length, 1, "should have a modal opened");
      assert.strictEqual($modal.find('.modal-body').text(),
          "The record has been modified, your changes will be discarded. Do you want to proceed?",
          "should warn the user that any unsaved changes will be lost");
  
      form.destroy();
  });
  
  QUnit.test('chatter: discard changes on opening full-composer and open missing partner info popup', async function (assert) {
      // When we open the full-composer, any following operations by the user
      // will reload the record (even closing the full-composer). Therefore,
      // we should warn the user when we open the full-composer if the record
      // is dirty (= has some unsaved changes).
      assert.expect(3);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"' +
                          ' options="{\'display_log_button\': True,' +
                          ' \'post_refresh\': \'always\'}"/>' +
                  '</div>' +
              '</form>',
          archs: {
              'res.partner,false,form':
                  '<form string="partners">' +
                  '<field name="name"/>' +
                  '</form>',
          },
          res_id: 2,
          session: {},
          mockRPC: function (route, args) {
              if (route === "/mail/get_suggested_recipients") {
                  return Promise.resolve({2: [[
                          false,
                          'pikapika@pikachu.com',
                          ''
                      ]]});
              }
              if (route === "/mail/get_partner_info") {
                  return Promise.resolve([{full_name: "pikapika@pikachu.com", partner_id: false}]);
              }
              return this._super(route, args);
          },
          viewOptions: {
              mode: 'edit',
          },
      });
  
      // Make record dirty
      await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu');
  
      // Open full-composer
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      await testUtils.dom.click(form.$('.o_composer_button_full_composer'));
  
      var $modal = $('.modal-dialog');
      assert.strictEqual($modal.length, 1, "should have a modal opened");
      assert.strictEqual($modal.find('.modal-body').text(),
          "The record has been modified, your changes will be discarded. Do you want to proceed?",
          "should warn the user that any unsaved changes will be lost");
      await testUtils.dom.click($modal.find('.modal-footer .btn.btn-primary'));
      assert.strictEqual($modal.length, 1, "should have a modal opened");
  
      form.destroy();
  });
  
  QUnit.test('chatter in x2many form view', async function (assert) {
      // the purpose of this test is to ensure that it doesn't crash when a x2many
      // record is opened in form view (thus in a dialog), and when there is a
      // chatter in the arch (typically, this may occur when the view used for the
      // x2many is a default one, which is also used in another context, as the
      // chatter is hidden in the dialog anyway)
      assert.expect(2);
  
      this.data.partner.fields.o2m = {
          string: "one2many field", type: "one2many", relation: 'partner',
      };
      this.data.partner.records[0].o2m = [2];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form><field name="o2m"/></form>',
          archs: {
              'partner,false,form': '<form>' +
                  '<field name="foo"/>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
              'partner,false,list': '<tree><field name="display_name"/></tree>',
          },
          res_id: 2,
          viewOptions: {
              mode: 'edit',
          },
      });
  
      await testUtils.dom.click(form.$('.o_data_row:first'));
  
      assert.strictEqual($('.modal .o_form_view').length, 1,
          "should have open a form view in a modal");
      assert.strictEqual($('.modal .o_chatter:visible').length, 0,
          "chatter should be hidden (as in a dialog)");
  
      form.destroy();
  });
  
  QUnit.test('chatter: Attachment viewer', async function (assert) {
      assert.expect(6);
      this.data.partner.records[0].message_ids = [1];
      this.data['mail.message'].records = [{
          attachment_ids: [{
              filename: 'image1.jpg',
              id:1,
              checksum: 999,
              mimetype: 'image/jpeg',
              name: 'Test Image 1',
              url: '/web/content/1?download=true'
          },{
              filename: 'image2.jpg',
              id:2,
              checksum: 999,
              mimetype: 'image/jpeg',
              name: 'Test Image 2',
              url: '/web/content/2?download=true'
          },{
              filename: 'image3.jpg',
              id:3,
              checksum: 999,
              mimetype: 'image/jpeg',
              name: 'Test Image 3',
              url: '/web/content/3?download=true'
          },{
              filename: 'pdf1.pdf',
              id:4,
              mimetype: 'application/pdf',
              name: 'Test PDF 1',
              url: '/web/content/4?download=true'
          }],
          author_id: ["1", "John Doe"],
          body: "Attachement viewer test",
          date: "2016-12-20 09:35:40",
          id: 1,
          is_note: false,
          is_discussion: true,
          is_starred: false,
          model: 'partner',
          res_id: 2,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread" options="{\'display_log_button\': True}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (_.str.contains(route, '/mail/attachment/preview/') ||
                  _.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')){
                  var canvas = document.createElement('canvas');
                  return Promise.resolve(canvas.toDataURL());
              }
              return this._super.apply(this, arguments);
          },
      });
      assert.containsN(form, '.o_thread_message .o_attachment', 4,
          "there should be three attachment on message");
      assert.hasAttrValue(form.$('.o_thread_message .o_attachment a').first(), 'href', '/web/content/1?download=true',
          "image caption should have correct download link");
      // click on first image attachement
      await testUtils.dom.click(form.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first());
      assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/1?unique=1&signature=999&model=ir.attachment"]').length, 1,
          "Modal popup should open with first image src");
      //  click on next button
      await testUtils.dom.click($('.modal .arrow.arrow-right.move_next span'));
      assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/2?unique=1&signature=999&model=ir.attachment"]').length, 1,
          "Modal popup should have now second image src");
      assert.strictEqual($('.o_modal_fullscreen .o_viewer_toolbar .o_download_btn').length, 1,
          "Modal popup should have download button");
      // close attachment popup
      await testUtils.dom.click($('.o_modal_fullscreen .o_viewer-header .o_close_btn'));
      // click on pdf attachement
      await testUtils.dom.click(form.$('span:contains(Test PDF 1)'));
      assert.strictEqual($('.o_modal_fullscreen iframe[data-src*="/web/content/4"]').length, 1,
          "Modal popup should open with the pdf preview");
      // close attachment popup
      await testUtils.dom.click($('.o_modal_fullscreen .o_viewer-header .o_close_btn'));
      form.destroy();
  });
  
  QUnit.test('chatter: keep context when sending a message', async function(assert) {
      assert.expect(1);
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {
              user_context: {lang: 'en_US'},
          },
          mockRPC: function (route, args) {
              if (route === '/mail/get_suggested_recipients') {
                  return Promise.resolve({2: []});
              }
              if (args.method === 'message_post') {
                  assert.deepEqual(args.kwargs.context, {
                          default_model: "partner",
                          default_res_id: 2,
                          lang: "en_US",
                          mail_post_autofollow: true,
                      },
                      "the context is incorrect");
                  return Promise.resolve(57923);
              }
              if (args.method === 'message_format') {
                  return Promise.resolve([{
                      author_id: [42, "Me"],
                      model: 'partner',
                  }]);
              }
              return this._super(route, args);
          },
      });
  
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      await testUtils.fields.editInput(form.$('.oe_chatter .o_composer_text_field:first()'), 'Pouet');
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
      form.destroy();
  });
  
  QUnit.test('form activity widget: read RPCs', async function (assert) {
      // Records of model 'mail.activity' may be updated in business flows (e.g.
      // the date of a 'Meeting' activity is updated when the associated meeting
      // is dragged&dropped in the Calendar view). Because of that, the activities
      // must be reloaded when the form is reloaded (the widget can't keep an
      // internal cache).
      assert.expect(6);
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 2,
      }];
  
      var nbReads = 0;
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<div class="oe_chatter">' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (args.method === 'activity_format' && args.model === 'mail.activity') {
                  nbReads++;
              }
              return this._super.apply(this, arguments);
          },
      });
  
      assert.strictEqual(nbReads, 1, "should have read the activities");
      assert.containsOnce(form, '.o_mail_activity .o_thread_message',
          "should display an activity");
      assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(),
          'Today', "the activity should be today");
  
      await testUtils.form.clickEdit(form);
      await testUtils.form.clickSave(form);
  
      assert.strictEqual(nbReads, 1, "should not have re-read the activities");
  
      // simulate a date change, and a reload of the form view
      var tomorrow = moment().add(1, 'day').format("YYYY-MM-DD");
      this.data['mail.activity'].records[0].date_deadline = tomorrow;
      await form.reload();
  
      assert.strictEqual(nbReads, 2, "should have re-read the activities");
      assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(),
          'Tomorrow', "the activity should be tomorrow");
  
      form.destroy();
  });
  
  QUnit.test('form activity widget on a new record', async function (assert) {
      assert.expect(0);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<div class="oe_chatter">' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          mockRPC: function (route, args) {
              if (args.method === 'read' && args.model === 'mail.activity') {
                  throw new Error("should not do a read on mail.activity");
              }
              return this._super.apply(this, arguments);
          },
      });
  
      form.destroy();
  });
  
  QUnit.test('form activity widget with another x2many field in view', async function (assert) {
      assert.expect(1);
  
      this.data.partner.fields.m2m = {string: "M2M", type: 'many2many', relation: 'partner'};
  
      this.data.partner.records[0].m2m = [2];
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 2,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<field name="m2m" widget="many2many_tags"/>' +
                  '<div class="oe_chatter">' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
      });
  
      assert.containsOnce(form, '.o_mail_activity .o_thread_message',
          "should display an activity");
  
      form.destroy();
  });
  
  QUnit.test('form activity widget: schedule next activity', async function (assert) {
      assert.expect(4);
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 2,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') {
                  assert.ok(_.isEqual(args.args[0], [1]), "should call 'action_feedback_schedule_next' for id 1");
                  assert.strictEqual(args.kwargs.feedback, 'everything is ok',
                      "the feedback should be sent correctly");
                  return Promise.resolve('test_result');
              }
              return this._super.apply(this, arguments);
          },
          intercepts: {
              do_action: function (event) {
                  assert.strictEqual(event.data.action,'test_result' , "should do a do_action with correct parameters");
                  event.data.options.on_close();
              },
          },
      });
      //Schedule next activity
      await testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=1]'));
      assert.containsOnce(form, '.o_mail_activity_feedback.popover',
          "a feedback popover should be visible");
      $('.o_mail_activity_feedback.popover textarea').val('everything is ok'); // write a feedback
      await testUtils.dom.click(form.$('.o_activity_popover_done_next'));
      form.destroy();
  });
  
  QUnit.test('form activity widget: do not close activity popover on click', async function (assert) {
      assert.expect(3);
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_user_id: 2,
          activity_type_id: 2,
      }];
  
      const form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: `<form string="Partners">
                  <sheet>
                      <field name="foo"/>
                  </sheet>
                  <div class="oe_chatter">
                      <field name="message_ids" widget="mail_thread"/>
                      <field name="activity_ids" widget="mail_activity"/>
                  </div>
              </form>`,
          res_id: 2,
          mockRPC(route, args) {
              if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') {
                  return Promise.resolve();
              }
              return this._super.apply(this, arguments);
          },
          intercepts: {
              do_action(ev) {
                  ev.data.options.on_close();
              },
          },
      });
  
      //Schedule next activity
      await testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=1]'));
  
      for (let i = 0; i < 2; i++) {
          // click twice in feedback area
          await testUtils.dom.click(form.$('.o_mail_activity_feedback.popover textarea'));
          const $popover = form.$('.o_mail_activity_feedback.popover');
          const $PopoverInDom = form.$(`#${$popover.attr('id')}`);
          assert.isVisible(
              $PopoverInDom,
              "the feedback popover should still be visible on click");
      }
      await concurrency.delay(250); // wait for popover transition
      const $popover = form.$('.o_mail_activity_feedback.popover');
      if (!$popover) {
          throw new Error("popover should be in DOM");
      }
      const $PopoverInDom = form.$(`#${$popover.attr('id')}`);
      assert.isVisible(
          $PopoverInDom,
          "the feedback popover should still be visible on click");
  
      form.destroy();
  });
  
  
  QUnit.test('form activity widget: edit next activity', async function (assert) {
      assert.expect(3);
      var self = this;
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 2,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          intercepts: {
              do_action: function (event) {
                  assert.deepEqual(event.data.action, {
                      context: {
                        default_res_id: 2,
                        default_res_model: "partner"
                      },
                      name: "Schedule Activity",
                      res_id: 1,
                      res_model: "mail.activity",
                      target: "new",
                      type: "ir.actions.act_window",
                      view_mode: "form",
                      views: [
                        [
                          false,
                          "form"
                        ]
                      ]
                    },
                    "should do a do_action with correct parameters");
                  self.data['mail.activity'].records[0].activity_type_id = 1;
                  event.data.options.on_close();
              },
          },
      });
      assert.strictEqual(form.$('.o_mail_activity .o_mail_info strong:eq(1)').text(), " Type 2",
          "Initial type should be Type 2");
      await testUtils.dom.click(form.$('.o_mail_activity .o_edit_activity[data-activity-id=1]'));
      assert.strictEqual(form.$('.o_mail_activity .o_mail_info strong:eq(1)').text(), " Type 1",
          "After edit type should be Type 1");
      form.destroy();
  });
  
  QUnit.test('form activity widget: clic mail template', async function (assert) {
      assert.expect(4);
      this.data.partner.records[0].activity_ids = [1];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          activity_type_id: 2,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (args.method === 'activity_format') {
                  return this._super.apply(this, arguments).then(function (res) {
                      res[0].mail_template_ids = [{ id: 100, name: 'Temp1' }];
                      return res;
                  });
              }
              return this._super.apply(this, arguments);
          },
          intercepts: {
              do_action: function (ev) {
                  assert.deepEqual(ev.data.action, {
                          name: _t('Compose Email'),
                          type: 'ir.actions.act_window',
                          res_model: 'mail.compose.message',
                          views: [[false, 'form']],
                          target: 'new',
                          context: {
                              default_res_id: 2,
                              default_model: 'partner',
                              default_use_template: true,
                              default_template_id: 100,
                              force_email: true,
                          },
                      },
                      "should do a do_action with correct parameters");
                      ev.data.options.on_close();
              },
          },
      });
      assert.containsOnce(form, '.o_mail_activity .o_thread_message',
          "we should have one activity");
      assert.containsOnce(form, '.o_activity_template_preview',
          "Activity should contains one mail template");
      await testUtils.dom.click(form.$('.o_activity_template_preview[data-template-id=100]'));
      assert.containsOnce(form, '.o_mail_activity .o_thread_message',
          "activity should still be there");
      form.destroy();
  });
  
  QUnit.test('form activity widget: schedule activity does not discard changes', async function (assert) {
      assert.expect(1);
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (args.method === 'write') {
                  assert.deepEqual(args.args[1], {foo: 'new value'},
                      "should correctly save the change");
              }
              return this._super.apply(this, arguments);
          },
          intercepts: {
              do_action: function (event) {
                  event.data.options.on_close();
              },
          },
          viewOptions: {
              mode: 'edit',
          },
      });
  
      // update value of foo field
      await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'new value');
  
      // schedule an activity (this triggers a do_action)
      await testUtils.dom.click(form.$('.o_chatter_button_schedule_activity'));
  
      // save the record
      await testUtils.form.clickSave(form);
  
      form.destroy();
  });
  
  QUnit.test('form activity widget: mark as done and remove', async function (assert) {
      assert.expect(15);
  
      var self = this;
  
      var nbReads = 0;
      this.data.partner.records[0].activity_ids = [1, 2];
      this.data.partner.records[0].activity_state = 'today';
      this.data['mail.activity'].records = [{
          id: 1,
          display_name: "An activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 1,
      }, {
          id: 2,
          display_name: "A second activity",
          date_deadline: moment().format("YYYY-MM-DD"), // now
          can_write: true,
          state: "today",
          user_id: 2,
          create_uid: 2,
          activity_type_id: 1,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (route === '/web/dataset/call_kw/mail.activity/unlink') {
                  assert.ok(_.isEqual(args.args[0], [1]), "should call 'unlink' for id 1");
              } else if (route === '/web/dataset/call_kw/mail.activity/action_feedback') {
                  assert.ok(_.isEqual(args.args[0], [2]), "should call 'action_feedback' for id 2");
                  assert.strictEqual(args.kwargs.feedback, 'everything is ok',
                      "the feedback should be sent correctly");
                  // should generate a message and unlink the activity
                  self.data.partner.records[0].message_ids = [1];
                  this.data['mail.message'].records.push({
                      attachment_ids: [],
                      author_id: ["1", "John Doe"],
                      body: "The activity has been done",
                      date: "2016-12-20 09:35:40",
                      id: 1,
                      is_note: true,
                      is_discussion: false,
                      model: 'partner',
                      res_id: 2,
                  });
                  route = '/web/dataset/call_kw/mail.activity/unlink';
                  args.method = 'unlink';
              } else if (route === '/web/dataset/call_kw/partner/read') {
                  nbReads++;
                  if (nbReads === 1) { // first read
                      assert.strictEqual(args.args[1].length, 5, 'should read all fiels the first time');
                  } else if (nbReads === 2) { // second read: after the unlink
                      assert.ok(_.isEqual(args.args[1], ['activity_ids', 'display_name']),
                          'should only read the activities (+ display_name) after an unlink');
                  } else { // third read: after marking an activity done
                      assert.ok(_.isEqual(args.args[1], ['activity_ids', 'message_ids', 'display_name']),
                          'should read the activities and messages (+ display_name) after marking an activity done');
                  }
              }
              return this._super.apply(this, arguments);
          },
      });
  
      assert.containsN(form, '.o_mail_activity .o_thread_message', 2,
          "there should be two activities");
  
      // remove activity 1
      await testUtils.dom.click(form.$('.o_mail_activity .o_unlink_activity[data-activity-id=1]'));
      assert.containsOnce(form, '.o_mail_activity .o_thread_message',
          "there should be one remaining activity");
      assert.ok(!form.$('.o_mail_activity .o_unlink_activity[data-activity-id=1]').length,
          "activity 1 should have been removed");
  
      // mark activity done
      assert.ok(!form.$('.o_mail_thread .o_thread_message').length,
          "there should be no chatter message");
      await testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=2]'));
      assert.containsOnce(form, '.o_mail_activity_feedback.popover',
          "a feedback popover should be visible");
      $('.o_mail_activity_feedback.popover textarea').val('everything is ok'); // write a feedback
      await testUtils.dom.click(form.$('.o_activity_popover_done'));
      assert.containsNone(form, '.o_mail_activity_feedback.popover')
      assert.ok(!form.$('.o_mail_activity .o_thread_message').length,
          "there should be no more activity");
      assert.containsOnce(form, '.o_mail_thread .o_thread_message',
          "a chatter message should have been generated");
      assert.strictEqual(form.$('.o_thread_message:contains(The activity has been done)').length, 1,
          "the message's body should be correct");
      form.destroy();
  });
  
  QUnit.test('followers widget: follow/unfollow, edit subtypes', async function (assert) {
      assert.expect(14);
  
      var resID = 2;
      var partnerID = 2;
      var followers = [];
      var nbReads = 0;
      var subtypes = [
          {id: 1, name: "First subtype", followed: true},
          {id: 2, name: "Second subtype", followed: true},
          {id: 3, name: "Third subtype", followed: false},
      ];
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                  '</div>' +
              '</form>',
          res_id: resID,
          mockRPC: function (route, args) {
              if (route === '/web/dataset/call_kw/partner/message_subscribe') {
                  assert.strictEqual(args.args[0][0], resID, 'should call route for correct record');
                  assert.ok(_.isEqual(args.kwargs.partner_ids, [partnerID]),
                      'should call route for correct partner');
                  if (args.kwargs.subtype_ids) {
                      // edit subtypes
                      assert.ok(_.isEqual(args.kwargs.subtype_ids, [1]),
                          'should call route with the correct subtypes');
                      _.each(subtypes, function (subtype) {
                          subtype.followed = _.contains(args.kwargs.subtype_ids, subtype.id);
                      });
                  } else {
                      // follow
                      this.data.partner.records[0].message_follower_ids = [1];
                      followers.push({
                          id: 1,
                          is_uid: true,
                          name: "Admin",
                          email: "admin@example.com",
                          res_id: resID,
                          res_model: 'partner',
                      });
                  }
                  return Promise.resolve(true);
              }
              if (route === '/mail/read_followers') {
                  return Promise.resolve({
                      followers: followers,
                      subtypes: subtypes,
                      // caution, subtype will only be returned if current user is in args follower list
                  });
              }
              if (route === '/web/dataset/call_kw/partner/message_unsubscribe') {
                  assert.strictEqual(args.args[0][0], resID, 'should call route for correct record');
                  assert.ok(_.isEqual(args.args[1], [partnerID]), 'should call route for correct partner');
                  this.data.partner.records[0].message_follower_ids = [];
                  followers = [];
                  return Promise.resolve(true);
              }
              if (route === '/web/dataset/call_kw/partner/read') {
                  nbReads++;
                  if (nbReads === 1) { // first read: should read all fields
                      assert.strictEqual(args.args[1].length, 3,
                          'should read "foo", "message_follower_ids" and "display_name"');
                  } else { // three next reads: only read 'message_follower_ids' field
                      assert.deepEqual(args.args[1], ['message_follower_ids', 'display_name'],
                          'should only read "message_follower_ids" and "display_name"');
                  }
              }
              return this._super.apply(this, arguments);
          },
          session: {partner_id: partnerID},
      });
  
      assert.strictEqual(form.$('.o_followers_count').text(), "0", 'should have no followers');
      assert.ok(form.$('.o_followers_follow_button.o_followers_notfollow').length,
          'should display the "Follow" button');
  
      // click to follow the document
      await testUtils.dom.click(form.$('.o_followers_follow_button'));
      assert.strictEqual(form.$('.o_followers_count').text(), "1", 'should have one follower');
      assert.ok(form.$('.o_followers_follow_button.o_followers_following').length,
          'should display the "Following/Unfollow" button');
      assert.containsOnce(form, '.o_followers_list .o_partner',
          "there should be one follower in the follower dropdown");
  
      // click to unfollow
      await testUtils.dom.click(form.$('.o_followers_follow_button'));
      assert.strictEqual(form.$('.o_followers_count').text(), "0", 'should have no followers');
      assert.ok(form.$('.o_followers_follow_button.o_followers_notfollow').length,
          'should display the "Follow" button');
  
      form.destroy();
  });
  
  QUnit.test('followers widget: follow/unfollow confirmation dialog', async function (assert) {
      assert.expect(5);
  
      this.data.partner.records[0].message_follower_ids = [1, 2];
      var resID = 2;
      var partnerID = 2;
      var followers = [{
          id: 1,
          is_uid: true,
          name: "Admin",
          email: "admin@example.com",
          res_id: resID,
          res_model: 'res.partner',
      }, {
          id: 2,
          is_uid: true,
          name: "Demo",
          email: "demo@example.com",
          res_id: 3,
          res_model: 'res.partner',
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                  '</div>' +
              '</form>',
          res_id: resID,
  
          mockRPC: function (route, args) {
              if (route === '/web/dataset/call_kw/partner/message_unsubscribe') {
                  this.data.partner.records[0].message_follower_ids.shift();
                  followers.shift();
                  return Promise.resolve(true);
              }
              if (route === '/mail/read_followers') {
                  return Promise.resolve({
                      followers: followers,
                  });
              }
              return this._super.apply(this, arguments);
          },
          session: {partner_id: partnerID},
      });
  
      assert.strictEqual(form.$('.o_followers_count').text(), "2", 'should have 2 followers');
      // click to unfollow current user
      await testUtils.dom.click(form.$('.o_followers_follow_button'));
      assert.containsNone($, '.modal', 'a confirm modal should not be opened');
      assert.strictEqual(form.$('.o_followers_count').text(), "1", 'should have 1 follower');
  
      await testUtils.dom.click(form.$('.o_followers_title_box [data-toggle="dropdown"]'));
      // remove demo user from followers
      await testUtils.dom.click(form.$('.o_remove_follower'));
      assert.containsOnce($, '.modal', 'a confirm modal should be opened');
      await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
      assert.strictEqual(form.$('.o_followers_count').text(), "0", 'should have no followers');
  
      form.destroy();
  });
  
  QUnit.test('followers widget: do not display follower duplications', async function (assert) {
      assert.expect(2);
  
      this.data.partner.records[0].message_follower_ids = [1];
      var resID = 2;
      var followers = [{
          id: 1,
          name: "Admin",
          email: "admin@example.com",
          res_id: resID,
          res_model: 'partner',
      }];
      var def;
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form>' +
                  '<sheet></sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                  '</div>' +
              '</form>',
          mockRPC: function (route, args) {
              if (route === '/mail/read_followers') {
                  return Promise.resolve(def).then(function () {
                      return {
                          followers: _.filter(followers, function (follower) {
                              return _.contains(args.follower_ids, follower.id);
                          }),
                          subtypes: [],
                      };
                  });
              }
              return this._super.apply(this, arguments);
          },
          res_id: resID,
          session: {partner_id: 1},
      });
  
  
      followers.push({
          id: 2,
          is_uid: false,
          name: "A follower",
          email: "follower@example.com",
          res_id: resID,
          res_model: 'partner',
      });
      this.data.partner.records[0].message_follower_ids.push(2);
  
      // simulate concurrent calls to read_followers and check that those followers
      // are not added twice in the dropdown
      def = testUtils.makeTestPromise();
      form.reload();
      form.reload();
      await def.resolve();
      await testUtils.nextTick();
      assert.strictEqual(form.$('.o_followers_count').text(), '2',
          "should have 2 followers");
      assert.containsN(form, '.o_followers_list .o_partner', 2,
          "there should be 2 followers in the follower dropdown");
  
      form.destroy();
  });
  
  QUnit.test('followers widget: display inactive followers with a different style', async function (assert) {
      assert.expect(6);
  
      this.data.partner.records[0].message_follower_ids = [1,2,3];
  
      var followers = [{
          id: 1,
          name: "Admin",
          email: "admin@example.com",
          res_id: 101,
          res_model: 'partner',
          active: true,
      },{
          id: 2,
          name: "Active_partner",
          email: "active_partner@example.com",
          res_id: 102,
          res_model: 'partner',
          active: true,
      },{
          id: 3,
          name: "Inactive_partner",
          email: "inactive_partner@example.com",
          res_id: 103,
          res_model: 'partner',
          active: false,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                  '</div>' +
              '</form>',
          mockRPC: function (route, args) {
              if (route === '/mail/read_followers') {
                  return Promise.resolve({
                      followers: _.filter(followers, function (follower) {
                          return _.contains(args.follower_ids, follower.id);
                      }),
                  });
              }
              return this._super.apply(this, arguments);
          },
          res_id: 2,
      });
  
      assert.doesNotHaveClass(form.$(".o_partner:has(a[data-oe-id='101'])"), 'o_inactive', 'Partner should be active');
      assert.hasAttrValue(form.$(".o_partner:has(a[data-oe-id='101']) > a"),'title','Admin');
      assert.doesNotHaveClass(form.$(".o_partner:has(a[data-oe-id='102'])"), 'o_inactive', 'Partner should be active');
      assert.hasAttrValue(form.$(".o_partner:has(a[data-oe-id='102']) > a"),'title','Active_partner');
      assert.hasClass(form.$(".o_partner:has(a[data-oe-id='103'])"), 'o_inactive', 'Partner should be inactive');
      assert.hasAttrValue(form.$(".o_partner:has(a[data-oe-id='103']) > a"),'title','Inactive_partner \n(inactive)');
  
      form.destroy();
  });
  
  QUnit.test('does not render and crash when destroyed before chat system is ready', async function (assert) {
      assert.expect(0);
  
      var def = testUtils.makeTestPromise();
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                      '<field name="message_ids" widget="mail_thread"/>' +
                      '<field name="activity_ids" widget="mail_activity"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (args.method === 'message_format') {
                  return Promise.resolve([{
                      attachment_ids: [],
                      body: "",
                      date: "2016-12-20 09:35:40",
                      id: 34,
                      res_id: 3,
                      author_id: ["3", "Fu Ck Mil Grom"],
                  }]);
              }
              if (route === '/mail/read_followers') {
                  return Promise.resolve({
                      followers: [],
                      subtypes: [],
                  });
              }
              return this._super(route, args);
          },
          intercepts: {
              get_session: function (event) {
                  event.stopPropagation();
                  event.data.callback({uid: 1, origin: 'http://web'});
              },
          },
      });
  
      form.destroy();
      // here, the chat service system is ready, and the chatter can try to render
      // itself. We simply make sure here that no crashes occur (since the form
      // view is destroyed, all rpcs will be dropped, and many other mechanisms
      // relying on events will not work, such as the chat bus)
      await def.resolve();
  });
  
  QUnit.test('chatter: do not duplicate messages on (un)star message', async function (assert) {
      assert.expect(4);
  
      this.data.partner.records[0].message_ids = [1];
      this.data['mail.message'].records = [{
          author_id: ["1", "John Doe"],
          body: "A message",
          date: "2016-12-20 09:35:40",
          id: 1,
          is_note: false,
          is_discussion: true,
          is_notification: false,
          is_starred: false,
          model: 'partner',
          res_id: 2,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread" options="{\'display_log_button\': True}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (args.method === 'toggle_message_starred') {
                  var messageData = _.findWhere(
                      this.data['mail.message'].records,
                      { id: args.args[0][0] }
                  );
                  messageData.is_starred = !messageData.is_starred;
                  // simulate notification received by mail_service from longpoll
                  var data = {
                      info: false,
                      message_ids: [messageData.id],
                      starred: messageData.is_starred,
                      type: 'toggle_star',
                  };
                  var notification = [[false, 'res.partner'], data];
                  form.call('bus_service', 'trigger', 'notification', [notification]);
                  return Promise.resolve();
              }
              return this._super(route, args);
          },
          session: {},
      });
  
      assert.containsOnce(form, '.o_thread_message',
          "there should be a single message in the chatter");
      assert.ok(form.$('.o_thread_message .o_thread_message_star.fa-star-o').length,
          "message should not be starred");
  
      // star message
      await testUtils.dom.click(form.$('.o_thread_message .o_thread_message_star'));
      assert.containsOnce(form, '.o_thread_message',
          "there should still be a single message in the chatter after starring the message");
  
      // unstar message
      await testUtils.dom.click(form.$('.o_thread_message .o_thread_message_star'));
      assert.containsOnce(form, '.o_thread_message',
          "there should still be a single message in the chatter after unstarring the message");
  
      //cleanup
      form.destroy();
  });
  
  QUnit.test('test open attachment option', async function (assert) {
      assert.expect(5);
  
      const form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          res_id: 2,
          arch: `
              <form string="Partners">
                  <sheet>
                      <field name="foo"/>
                  </sheet>
                  <div class="oe_chatter">
                      <field name="message_ids" widget="mail_thread" options="{'open_attachments': True}"/>
                  </div>
              </form>`,
      });
      assert.containsOnce(form, '.o_mail_chatter_attachments', "Attachment box should be open by default")
      await testUtils.form.clickEdit(form);
      assert.containsOnce(form, '.o_mail_chatter_attachments', "Attachment box should still be open")
      await testUtils.fields.editInput(form.$('.o_field_char'), 'Coucou Petite Perruche');
      assert.containsOnce(form, '.o_mail_chatter_attachments', "Attachment box should still be open")
  
      // Close the attachment box with the button
      await testUtils.dom.click(form.$('.o_chatter_button_attachment'))
      assert.containsNone(form, '.o_mail_chatter_attachments', "Attachment box should be closed")
      await testUtils.form.clickSave(form);
      assert.containsNone(form, '.o_mail_chatter_attachments', "Attachment box should still be closed")
  
      form.destroy();
  });
  
  QUnit.test('chatter: new messages on document without any "display_name"', async function (assert) {
      assert.expect(5);
  
      this.data.partner.records[0].message_ids = [1];
      this.data.partner.records[0].display_name = false;
      this.data['mail.message'].records = [{
          author_id: [1, "John Doe"],
          body: "A message",
          date: "2016-12-20 09:35:40",
          id: 1,
          is_note: false,
          is_discussion: true,
          is_notification: false,
          is_starred: false,
          model: 'partner',
          res_id: 2,
      }];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_ids" widget="mail_thread" options="{\'display_log_button\': True}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          session: {},
      });
  
      assert.containsOnce(form, '.o_thread_message',
          "should have a single message in the chatter");
      assert.containsOnce(form, '.o_thread_message[data-message-id="1"]',
          "single message should have ID 1");
  
      // Simulate a new message in the chatter
      this.data['mail.message'].records.push({
          author_id: [2, "Mister Smith"],
          body: "Second message",
          date: "2016-12-20 09:35:40",
          id: 2,
          is_note: false,
          is_discussion: true,
          is_notification: false,
          is_starred: false,
          model: 'partner',
          res_id: 2,
      });
      this.data.partner.records[0].message_ids.push(2);
  
      await form.reload();
  
      assert.containsN(form, '.o_thread_message', 2,
          "should have a two messages in the chatter after reload");
      assert.containsOnce(form, '.o_thread_message[data-message-id="1"]',
          "one of the message should have ID 1");
      assert.containsOnce(form, '.o_thread_message[data-message-id="2"]',
          "the other message should have ID 2");
  
      //cleanup
      form.destroy();
  });
  
  QUnit.test('chatter: suggested partner auto-follow on message post', async function (assert) {
      // need post_refresh 'recipient' to auto-follow suggested recipients
      // whose checkbox is checked.
      assert.expect(20);
  
      var self = this;
      this.data.partner.records[0].message_follower_ids = [1];
      this.data.partner.records[0].message_ids = [1];
      this.data['mail.message'].records = [{
          author_id: ["1", "John Doe"],
          body: "A message",
          date: "2016-12-20 09:35:40",
          id: 1,
          is_note: false,
          is_discussion: true,
          is_notification: false,
          is_starred: false,
          model: 'partner',
          res_id: 2,
      }];
  
      var followers = [];
      followers.push({
          id: 1,
          is_uid: true,
          name: "Admin",
          email: "admin@example.com",
          res_id: 5,
          res_model: 'partner',
      });
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                      '<field name="message_ids" widget="mail_thread" options="{\'post_refresh\': \'recipients\'}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (route === '/mail/get_suggested_recipients') {
                  return Promise.resolve({2: [
                          [
                              8,
                              'DemoUser <demo-user@example.com>',
                              'Customer Email',
                          ],
                      ]
                  });
              }
              if (args.method === 'message_post') {
                  assert.ok(args.kwargs.context.mail_post_autofollow,
                      "should autofollow checked suggested partners when posting message");
                  assert.deepEqual(args.kwargs.partner_ids, [8],
                      "should have provided Demo User to auto-follow chatter on message_post");
  
                  // add demo user in followers
                  self.data.partner.records[0].message_follower_ids.push(2);
                  followers.push({
                      id: 2,
                      is_uid: true,
                      name: "Demo User",
                      email: "demo-user@example.com",
                      res_id: 8,
                      res_model: 'partner',
                  });
  
                  // post a legit message so that it does not crashes
                  var lastMessageData = _.max(this.data['mail.message'].records, function (messageData) {
                      return messageData.id;
                  });
                  var messageID = lastMessageData.id + 1;
                  this.data['mail.message'].records.push({
                      author_id: ["42", "Me"],
                      body: args.kwargs.body,
                      date: "2016-12-20 10:35:40",
                      id: messageID,
                      is_note: args.kwargs.subtype === 'mail.mt_note',
                      is_discussion: args.kwargs.subtype === 'mail.mt_comment',
                      is_notification: false,
                      is_starred: false,
                      model: 'partner',
                      res_id: 2,
                  });
                  return Promise.resolve(messageID);
              }
              if (route === '/mail/read_followers') {
                  return Promise.resolve({
                      followers: followers,
                  });
              }
              return this._super(route, args);
          },
          session: {},
      });
  
      assert.containsOnce(form, '.o_thread_message', "thread should contain one message");
      assert.ok(form.$('.o_thread_message:first().o_mail_discussion').length,
          "the message should be a discussion");
      assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('A message') >= 0,
          "the message's body should be correct");
      assert.ok(form.$('.o_thread_message:first() .o_mail_info').text().indexOf('John Doe') >= 0,
          "the message's author should be correct");
      assert.containsOnce(form, '.o_followers',
          "should display follower widget");
      assert.strictEqual(form.$('.o_followers_count').text(), "1",
          "should have a single follower (widget counter)");
      assert.containsOnce(form, '.o_followers_list > div.o_partner',
          "should have a single follower (listed partners)");
      assert.strictEqual(form.$('.o_followers_list > div.o_partner > a').text().trim(), "Admin",
          "should have 'Admin' as follower");
  
      // open composer
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.isVisible($('.oe_chatter .o_thread_composer'), "chatter should be opened");
      assert.strictEqual($('.o_composer_suggested_partners').length, 1,
          "should display suggested partners");
      assert.strictEqual($('.o_composer_suggested_partners > div').length, 1,
          "should display 1 suggested partner");
      assert.ok($('.o_composer_suggested_partners input').is(':checked'),
          "should have checkbox that is checked by default");
      assert.strictEqual($('.o_composer_suggested_partners input').data('fullname'),
          "DemoUser <demo-user@example.com>",
          "should have partner suggestion with correct fullname (data)");
      assert.strictEqual($('.o_composer_suggested_partners label').text().replace(/\s+/g, ''),
          "DemoUser(demo-user@example.com)",
          "should have partner suggestion with correct fullname (rendering)");
  
      // send message
      form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
      await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send'));
  
      assert.strictEqual(form.$('.o_followers_count').text(), "2",
          "should have a two followers (widget counter)");
      assert.containsN(form, '.o_followers_list > div.o_partner', 2,
          "should have two followers (listed partners)");
      assert.strictEqual(form.$('.o_followers_list > div.o_partner > a[data-oe-id="5"]').text().trim(),
          "Admin",
          "should have 'Admin' as follower");
      assert.strictEqual(form.$('.o_followers_list > div.o_partner > a[data-oe-id="8"]').text().trim(),
          "Demo User",
          "should have 'Demo User' as follower");
  
      //cleanup
      form.destroy();
  });
  
  QUnit.test('chatter: mention prefetched partners (followers & employees)', async function (assert) {
      // Note: employees are in prefeteched partner for mentions in chatter when
      // the module hr is installed.
      assert.expect(10);
  
      var followerSuggestions = [{
          id: 1,
          name: 'FollowerUser1',
          email: 'follower-user1@example.com',
      }, {
          id: 2,
          name: 'FollowerUser2',
          email: 'follower-user2@example.com',
      }];
  
      var nonFollowerSuggestions = [{
          id: 3,
          name: 'NonFollowerUser1',
          email: 'non-follower-user1@example.com',
      }, {
          id: 4,
          name: 'NonFollowerUser2',
          email: 'non-follower-user2@example.com',
      }];
  
      // link followers
      this.data.partner.records[0].message_follower_ids = [10, 20];
  
      // prefetched partners
      this.data.initMessaging = {
          mention_partner_suggestions: [followerSuggestions.concat(nonFollowerSuggestions)],
      };
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: '<form string="Partners">' +
                  '<sheet>' +
                      '<field name="foo"/>' +
                  '</sheet>' +
                  '<div class="oe_chatter">' +
                      '<field name="message_follower_ids" widget="mail_followers"/>' +
                      '<field name="message_ids" widget="mail_thread" options="{\'display_log_button\': True}"/>' +
                  '</div>' +
              '</form>',
          res_id: 2,
          mockRPC: function (route, args) {
              if (route === '/mail/read_followers') {
                  return Promise.resolve({
                      followers: [{
                          id: 10,
                          name: 'FollowerUser1',
                          email: 'follower-user1@example.com',
                          res_model: 'res.partner',
                          res_id: 1,
                      }, {
                          id: 20,
                          name: 'FollowerUser2',
                          email: 'follower-user2@example.com',
                          res_model: 'res.partner',
                          res_id: 2,
                      }],
                      subtypes: [],
                  });
              }
              if (route === '/mail/get_suggested_recipients') {
                  return Promise.resolve({2: []});
              }
              if (args.method === 'get_mention_suggestions') {
                  throw new Error('should not fetch partners for mentions');
              }
              return this._super(route, args);
          },
          session: {},
      });
  
      assert.strictEqual(form.$('.o_followers_count').text(), '2',
          "should have two followers of this document");
      assert.strictEqual(form.$('.o_followers_list > .o_partner').text().replace(/\s+/g, ''),
          'FollowerUser1FollowerUser2',
          "should have correct follower names");
      assert.strictEqual(form.$('.o_composer_mention_dropdown').length, 0,
          "should not show the mention suggestion dropdown");
  
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      var $input = form.$('.oe_chatter .o_composer_text_field:first()');
      $input.val('@');
      // the cursor position must be set for the mention manager to detect that we are mentionning
      $input[0].selectionStart = 1;
      $input[0].selectionEnd = 1;
      $input.trigger('keyup');
      await testUtils.nextTick();
      assert.strictEqual(form.$('.o_composer_mention_dropdown').length, 1,
          "should show the mention suggestion dropdown");
  
      assert.strictEqual(form.$('.o_mention_proposition').length, 4,
          "should show 4 mention suggestions");
      assert.strictEqual(form.$('.o_mention_proposition').eq(0).text().replace(/\s+/g, ''),
          "FollowerUser1(follower-user1@example.com)",
          "should display correct 1st mention suggestion");
      assert.strictEqual(form.$('.o_mention_proposition').eq(1).text().replace(/\s+/g, ''),
          "FollowerUser2(follower-user2@example.com)",
          "should display correct 2nd mention suggestion");
      assert.ok(form.$('.o_mention_proposition').eq(1).next().hasClass('dropdown-divider'),
          "should have a mention separator after last follower mention suggestion");
      assert.strictEqual(form.$('.o_mention_proposition').eq(2).text().replace(/\s+/g, ''),
          "NonFollowerUser1(non-follower-user1@example.com)",
          "should display correct 3rd mention suggestion");
      assert.strictEqual(form.$('.o_mention_proposition').eq(3).text().replace(/\s+/g, ''),
          "NonFollowerUser2(non-follower-user2@example.com)",
          "should display correct 4th mention suggestion");
  
      //cleanup
      form.destroy();
  });
  
  QUnit.test('chatter: display suggested partners only once', async function (assert) {
      assert.expect(3);
  
      let isMessageSent = false;
  
      const form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          services: this.services,
          arch: `
              <form string="Partners">
                  <sheet>
                      <field name="foo"/>
                  </sheet>
                  <div class="oe_chatter">
                      <field name="message_follower_ids" widget="mail_followers"/>
                      <field name="message_ids" widget="mail_thread" options="{'display_log_button': True}"/>
                  </div>
              </form>`,
          res_id: 2,
          async mockRPC(route, args) {
              if (route === '/mail/read_followers') {
                  return {
                      followers: [],
                      subtypes: [],
                  };
              }
              if (route === '/mail/get_suggested_recipients') {
                  if (isMessageSent) {
                      return {
                          2: [],
                      };
                  }
                  return {
                      2: [[2, "Jack <jack@example.com>", "Partner Profile"]],
                  };
              }
              if (args.method === 'message_post') {
                  isMessageSent = true;
                  return 57923;
              }
              if (args.method === 'message_format') {
                  return [{
                      author_id: ["42", "Me"],
                      model: 'partner',
                  }];
              }
              return this._super(route, args);
          },
          session: {},
      });
  
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      const $input = form.$('.oe_chatter .o_composer_text_field:first()');
      assert.containsOnce(
          form,
          'div.o_composer_suggested_partners input',
          "should show one suggested recipient");
      assert.strictEqual(
          form.$('div.o_composer_suggested_partners label').text().replace(/\s+/g, ''),
          "Jack(jack@example.com)",
          "should have the correct label");
  
      $input.val("BBQ and beers tonight! Are you in?");
      await testUtils.dom.click(form.$('.o_composer_button_send'));
      // Open composer for new message
      await testUtils.dom.click(form.$('.o_chatter_button_new_message'));
      assert.containsNone(
          form,
          'div.o_composer_suggested_partners input',
          "should no longer show the suggested recipient");
  
      form.destroy();
  });
  
  QUnit.module('FieldMany2ManyTagsEmail', {
      beforeEach: function () {
          this.data = {
              partner: {
                  fields: {
                      display_name: { string: "Displayed name", type: "char" },
                      timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
                  },
                  records: [{
                      id: 1,
                      display_name: "first record",
                      timmy: [],
                  }],
              },
              partner_type: {
                  fields: {
                      name: {string: "Partner Type", type: "char"},
                      email: {string: "Email", type: "char"},
                  },
                  records: [
                      {id: 12, display_name: "gold", email: 'coucou@petite.perruche'},
                      {id: 14, display_name: "silver", email: ''},
                  ]
              },
          };
      },
  });
  
  QUnit.test('fieldmany2many tags email', function (assert) {
      assert.expect(13);
      var done = assert.async();
  
      this.data.partner.records[0].timmy = [12, 14];
  
      // the modals need to be closed before the form view rendering
      createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          res_id: 1,
          arch:'<form string="Partners">' +
                  '<sheet>' +
                      '<field name="display_name"/>' +
                      '<field name="timmy" widget="many2many_tags_email"/>' +
                  '</sheet>' +
              '</form>',
          viewOptions: {
              mode: 'edit',
          },
          mockRPC: function (route, args) {
              if (args.method ==='read' && args.model === 'partner_type') {
                  assert.step(JSON.stringify(args.args[0]));
                  assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email");
              }
              return this._super.apply(this, arguments);
          },
          archs: {
              'partner_type,false,form': '<form string="Types"><field name="display_name"/><field name="email"/></form>',
          },
      }).then(async function (form) {
          // should read it 3 times (1 with the form view, one with the form dialog and one after save)
          assert.verifySteps(['[12,14]', '[14]', '[14]']);
          await testUtils.nextTick();
          assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0', 2,
              "two tags should be present");
          var firstTag = form.$('.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0').first();
          assert.strictEqual(firstTag.find('.o_badge_text').text(), "gold",
              "tag should only show display_name");
          assert.hasAttrValue(firstTag.find('.o_badge_text'), 'title', "coucou@petite.perruche",
              "tag should show email address on mouse hover");
          form.destroy();
          done();
      });
      testUtils.nextTick().then(function() {
          assert.strictEqual($('.modal-body.o_act_window').length, 1,
              "there should be one modal opened to edit the empty email");
          assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver",
              "the opened modal should be a form view dialog with the partner_type 14");
          assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1,
              "there should be an email field in the modal");
  
          // set the email and save the modal (will render the form view)
          testUtils.fields.editInput($('.modal-body.o_act_window input[name="email"]'), 'coucou@petite.perruche');
          testUtils.dom.click($('.modal-footer .btn-primary'));
      });
  
  });
  
  QUnit.test('fieldmany2many tags email (edition)', async function (assert) {
      assert.expect(15);
  
      this.data.partner.records[0].timmy = [12];
  
      var form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          res_id: 1,
          arch:'<form string="Partners">' +
                  '<sheet>' +
                      '<field name="display_name"/>' +
                      '<field name="timmy" widget="many2many_tags_email"/>' +
                  '</sheet>' +
              '</form>',
          viewOptions: {
              mode: 'edit',
          },
          mockRPC: function (route, args) {
              if (args.method ==='read' && args.model === 'partner_type') {
                  assert.step(JSON.stringify(args.args[0]));
                  assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email");
              }
              return this._super.apply(this, arguments);
          },
          archs: {
              'partner_type,false,form': '<form string="Types"><field name="display_name"/><field name="email"/></form>',
          },
      });
  
      assert.verifySteps(['[12]']);
      assert.containsOnce(form, '.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0',
          "should contain one tag");
  
      // add an other existing tag
      await testUtils.fields.many2one.clickOpenDropdown('timmy');
      await testUtils.fields.many2one.clickHighlightedItem('timmy');
  
      assert.strictEqual($('.modal-body.o_act_window').length, 1,
          "there should be one modal opened to edit the empty email");
      assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver",
          "the opened modal in edit mode should be a form view dialog with the partner_type 14");
      assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1,
          "there should be an email field in the modal");
  
      // set the email and save the modal (will rerender the form view)
      await testUtils.fields.editInput($('.modal-body.o_act_window input[name="email"]'), 'coucou@petite.perruche');
      await testUtils.dom.click($('.modal-footer .btn-primary'));
  
      assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0', 2,
          "should contain the second tag");
      // should have read [14] three times: when opening the dropdown, when opening the modal, and
      // after the save
      assert.verifySteps(['[14]', '[14]', '[14]']);
  
      form.destroy();
  });
  
  QUnit.test('many2many_tags_email widget can load more than 40 records', async function (assert) {
      assert.expect(3);
  
      this.data.partner.fields.partner_ids = {string: "Partner", type: "many2many", relation: 'partner'};
      this.data.partner.records[0].partner_ids = [];
      for (let i = 100; i < 200; i++) {
          this.data.partner.records.push({id: i, display_name: `partner${i}`});
          this.data.partner.records[0].partner_ids.push(i);
      }
  
      const form = await createView({
          View: FormView,
          model: 'partner',
          data: this.data,
          arch: '<form><field name="partner_ids" widget="many2many_tags"/></form>',
          res_id: 1,
      });
  
      assert.strictEqual(form.$('.o_field_widget[name="partner_ids"] .badge').length, 100);
  
      await testUtils.form.clickEdit(form);
  
      assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
  
      // add a record to the relation
      await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
      await testUtils.fields.many2one.clickHighlightedItem('partner_ids');
  
      assert.strictEqual(form.$('.o_field_widget[name="partner_ids"] .badge').length, 101);
  
      form.destroy();
  });
  
  });
  });
  