odoo.define('mail/static/src/widgets/form_renderer/form_renderer_tests.js', function (require) { "use strict"; const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js'); const { afterEach, afterNextRender, beforeEach, nextAnimationFrame, start, } = require('mail/static/src/utils/test_utils.js'); const config = require('web.config'); const FormView = require('web.FormView'); const { dom: { triggerEvent }, } = require('web.test_utils'); QUnit.module('mail', {}, function () { QUnit.module('widgets', {}, function () { QUnit.module('form_renderer', {}, function () { QUnit.module('form_renderer_tests.js', { beforeEach() { beforeEach(this); // FIXME archs could be removed once task-2248306 is done // The mockServer will try to get the list view // of every relational fields present in the main view. // In the case of mail fields, we don't really need them, // but they still need to be defined. this.createView = async (viewParams, ...args) => { await afterNextRender(async () => { const viewArgs = Object.assign( { archs: { 'mail.activity,false,list': '', 'mail.followers,false,list': '', 'mail.message,false,list': '', }, }, viewParams, ); const { afterEvent, env, widget } = await start(viewArgs, ...args); this.afterEvent = afterEvent; this.env = env; this.widget = widget; }); }; }, afterEach() { afterEach(this); }, }); QUnit.test('[technical] spinner when messaging is not created', async function (assert) { /** * Creation of messaging in env is async due to generation of models being * async. Generation of models is async because it requires parsing of all * JS modules that contain pieces of model definitions. * * Time of having no messaging is very short, almost imperceptible by user * on UI, but the display should not crash during this critical time period. */ assert.expect(3); this.data['res.partner'].records.push({ display_name: "second partner", id: 12, }); await this.createView({ data: this.data, hasView: true, messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation waitUntilMessagingCondition: 'none', // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.containsOnce( document.body, '.o_ChatterContainer', "should display chatter container even when messaging is not created yet" ); assert.containsOnce( document.body, '.o_ChatterContainer_noChatter', "chatter container should not display any chatter when messaging not created" ); assert.strictEqual( document.querySelector('.o_ChatterContainer').textContent, "Please wait...", "chatter container should display spinner when messaging not yet created" ); }); QUnit.test('[technical] keep spinner on transition from messaging non-created to messaging created (and non-initialized)', async function (assert) { /** * Creation of messaging in env is async due to generation of models being * async. Generation of models is async because it requires parsing of all * JS modules that contain pieces of model definitions. * * Time of having no messaging is very short, almost imperceptible by user * on UI, but the display should not crash during this critical time period. */ assert.expect(4); const messagingBeforeCreationDeferred = makeDeferred(); this.data['res.partner'].records.push({ display_name: "second partner", id: 12, }); await this.createView({ data: this.data, hasView: true, messagingBeforeCreationDeferred, async mockRPC(route, args) { const _super = this._super.bind(this, ...arguments); // limitation of class.js if (route === '/mail/init_messaging') { await new Promise(() => {}); // simulate messaging never initialized } return _super(); }, waitUntilMessagingCondition: 'none', // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.strictEqual( document.querySelector('.o_ChatterContainer').textContent, "Please wait...", "chatter container should display spinner when messaging not yet created" ); assert.containsOnce( document.body, '.o_ChatterContainer_noChatter', "chatter container should not display any chatter when messaging not created" ); // simulate messaging become created messagingBeforeCreationDeferred.resolve(); await nextAnimationFrame(); assert.strictEqual( document.querySelector('.o_ChatterContainer').textContent, "Please wait...", "chatter container should still display spinner when messaging is created but not initialized" ); assert.containsOnce( document.body, '.o_ChatterContainer_noChatter', "chatter container should still not display any chatter when messaging not initialized" ); }); QUnit.test('spinner when messaging is created but not initialized', async function (assert) { assert.expect(3); this.data['res.partner'].records.push({ display_name: "second partner", id: 12, }); await this.createView({ data: this.data, hasView: true, async mockRPC(route, args) { const _super = this._super.bind(this, ...arguments); // limitation of class.js if (route === '/mail/init_messaging') { await new Promise(() => {}); // simulate messaging never initialized } return _super(); }, waitUntilMessagingCondition: 'created', // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.containsOnce( document.body, '.o_ChatterContainer', "should display chatter container even when messaging is not fully initialized" ); assert.containsOnce( document.body, '.o_ChatterContainer_noChatter', "chatter container should not display any chatter when messaging not initialized" ); assert.strictEqual( document.querySelector('.o_ChatterContainer').textContent, "Please wait...", "chatter container should display spinner when messaging not yet initialized" ); }); QUnit.test('transition non-initialized messaging to initialized messaging: display spinner then chatter', async function (assert) { assert.expect(3); const messagingBeforeInitializationDeferred = makeDeferred(); this.data['res.partner'].records.push({ display_name: "second partner", id: 12, }); await this.createView({ data: this.data, hasView: true, async mockRPC(route, args) { const _super = this._super.bind(this, ...arguments); // limitation of class.js if (route === '/mail/init_messaging') { await messagingBeforeInitializationDeferred; } return _super(); }, waitUntilMessagingCondition: 'created', // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.strictEqual( document.querySelector('.o_ChatterContainer').textContent, "Please wait...", "chatter container should display spinner when messaging not yet initialized" ); assert.containsOnce( document.body, '.o_ChatterContainer_noChatter', "chatter container should not display any chatter when messaging not initialized" ); // Simulate messaging becomes initialized await afterNextRender(() => messagingBeforeInitializationDeferred.resolve()); assert.containsNone( document.body, '.o_ChatterContainer_noChatter', "chatter container should now display chatter when messaging becomes initialized" ); }); QUnit.test('basic chatter rendering', async function (assert) { assert.expect(1); this.data['res.partner'].records.push({ display_name: "second partner", id: 12, }); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.strictEqual( document.querySelectorAll(`.o_Chatter`).length, 1, "there should be a chatter" ); }); QUnit.test('basic chatter rendering without followers', async function (assert) { assert.expect(6); this.data['res.partner'].records.push({ display_name: "second partner", id: 12 }); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.containsOnce( document.body, '.o_Chatter', "there should be a chatter" ); assert.containsOnce( document.body, '.o_ChatterTopbar', "there should be a chatter topbar" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonAttachments', "there should be an attachment button" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonScheduleActivity', "there should be a schedule activity button" ); assert.containsNone( document.body, '.o_FollowerListMenu', "there should be no followers menu" ); assert.containsOnce( document.body, '.o_Chatter_thread', "there should be a thread" ); }); QUnit.test('basic chatter rendering without activities', async function (assert) { assert.expect(6); this.data['res.partner'].records.push({ display_name: "second partner", id: 12 }); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.containsOnce( document.body, '.o_Chatter', "there should be a chatter" ); assert.containsOnce( document.body, '.o_ChatterTopbar', "there should be a chatter topbar" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonAttachments', "there should be an attachment button" ); assert.containsNone( document.body, '.o_ChatterTopbar_buttonScheduleActivity', "there should be a schedule activity button" ); assert.containsOnce( document.body, '.o_FollowerListMenu', "there should be a followers menu" ); assert.containsOnce( document.body, '.o_Chatter_thread', "there should be a thread" ); }); QUnit.test('basic chatter rendering without messages', async function (assert) { assert.expect(6); this.data['res.partner'].records.push({ display_name: "second partner", id: 12 }); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', arch: `
`, res_id: 12, }); assert.containsOnce( document.body, '.o_Chatter', "there should be a chatter" ); assert.containsOnce( document.body, '.o_ChatterTopbar', "there should be a chatter topbar" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonAttachments', "there should be an attachment button" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonScheduleActivity', "there should be a schedule activity button" ); assert.containsOnce( document.body, '.o_FollowerListMenu', "there should be a followers menu" ); assert.containsNone( document.body, '.o_Chatter_thread', "there should be a thread" ); }); QUnit.test('chatter updating', async function (assert) { assert.expect(1); this.data['mail.message'].records.push({ body: "not empty", model: 'res.partner', res_id: 12 }); this.data['res.partner'].records.push( { display_name: "first partner", id: 11 }, { display_name: "second partner", id: 12 } ); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', res_id: 11, viewOptions: { ids: [11, 12], index: 0, }, arch: `
`, waitUntilEvent: { eventName: 'o-thread-view-hint-processed', message: "should wait until partner 11 thread loaded messages initially", predicate: ({ hint, threadViewer }) => { return ( hint.type === 'messages-loaded' && threadViewer.thread.model === 'res.partner' && threadViewer.thread.id === 11 ); }, } }); await this.afterEvent({ eventName: 'o-thread-view-hint-processed', func: () => document.querySelector('.o_pager_next').click(), message: "should wait until partner 12 thread loaded messages after clicking on next", predicate: ({ hint, threadViewer }) => { return ( hint.type === 'messages-loaded' && threadViewer.thread.model === 'res.partner' && threadViewer.thread.id === 12 ); }, }); assert.containsOnce( document.body, '.o_Message', "there should be a message in partner 12 thread" ); }); QUnit.test('chatter should become enabled when creation done', async function (assert) { assert.expect(10); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', arch: `
`, viewOptions: { mode: 'edit', }, }); assert.containsOnce( document.body, '.o_Chatter', "there should be a chatter" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonSendMessage', "there should be a send message button" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonLogNote', "there should be a log note button" ); assert.containsOnce( document.body, '.o_ChatterTopbar_buttonLogNote', "there should be an attachments button" ); assert.ok( document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, "send message button should be disabled" ); assert.ok( document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, "log note button should be disabled" ); assert.ok( document.querySelector(`.o_ChatterTopbar_buttonAttachments`).disabled, "attachments button should be disabled" ); document.querySelectorAll('.o_field_char')[0].focus(); document.execCommand('insertText', false, "hello"); await afterNextRender(() => { document.querySelector('.o_form_button_save').click(); }); assert.notOk( document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, "send message button should now be enabled" ); assert.notOk( document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, "log note button should now be enabled" ); assert.notOk( document.querySelector(`.o_ChatterTopbar_buttonAttachments`).disabled, "attachments button should now be enabled" ); }); QUnit.test('read more/less links are not duplicated when switching from read to edit mode', async function (assert) { assert.expect(5); this.data['mail.message'].records.push({ author_id: 100, // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks body: `
Dear Joel Willis,
Thank you for your enquiry.
If you have any questions, please let us know.

Thank you,
--
System
`, id: 1000, model: 'res.partner', res_id: 2, }); this.data['res.partner'].records.push({ display_name: "Someone", id: 100, }); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', res_id: 2, arch: `
`, waitUntilEvent: { eventName: 'o-component-message-read-more-less-inserted', message: "should wait until read more/less is inserted initially", predicate: ({ message }) => message.id === 1000, }, }); assert.containsOnce( document.body, '.o_Chatter', "there should be a chatter" ); assert.containsOnce( document.body, '.o_Message', "there should be a message" ); assert.containsOnce( document.body, '.o_Message_readMoreLess', "there should be only one read more" ); await afterNextRender(() => this.afterEvent({ eventName: 'o-component-message-read-more-less-inserted', func: () => document.querySelector('.o_form_button_edit').click(), message: "should wait until read more/less is inserted after clicking on edit", predicate: ({ message }) => message.id === 1000, })); assert.containsOnce( document.body, '.o_Message_readMoreLess', "there should still be only one read more after switching to edit mode" ); await afterNextRender(() => this.afterEvent({ eventName: 'o-component-message-read-more-less-inserted', func: () => document.querySelector('.o_form_button_cancel').click(), message: "should wait until read more/less is inserted after canceling edit", predicate: ({ message }) => message.id === 1000, })); assert.containsOnce( document.body, '.o_Message_readMoreLess', "there should still be only one read more after switching back to read mode" ); }); QUnit.test('read more links becomes read less after being clicked', async function (assert) { assert.expect(6); this.data['mail.message'].records = [{ author_id: 100, // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks body: `
Dear Joel Willis,
Thank you for your enquiry.
If you have any questions, please let us know.

Thank you,
--
System
`, id: 1000, model: 'res.partner', res_id: 2, }]; this.data['res.partner'].records.push({ display_name: "Someone", id: 100, }); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', res_id: 2, arch: `
`, waitUntilEvent: { eventName: 'o-component-message-read-more-less-inserted', message: "should wait until read more/less is inserted initially", predicate: ({ message }) => message.id === 1000, }, }); assert.containsOnce( document.body, '.o_Chatter', "there should be a chatter" ); assert.containsOnce( document.body, '.o_Message', "there should be a message" ); assert.containsOnce( document.body, '.o_Message_readMoreLess', "there should be a read more" ); assert.strictEqual( document.querySelector('.o_Message_readMoreLess').textContent, 'read more', "read more/less link should contain 'read more' as text" ); await afterNextRender(() => this.afterEvent({ eventName: 'o-component-message-read-more-less-inserted', func: () => document.querySelector('.o_form_button_edit').click(), message: "should wait until read more/less is inserted after clicking on edit", predicate: ({ message }) => message.id === 1000, })); assert.strictEqual( document.querySelector('.o_Message_readMoreLess').textContent, 'read more', "read more/less link should contain 'read more' as text" ); document.querySelector('.o_Message_readMoreLess').click(); assert.strictEqual( document.querySelector('.o_Message_readMoreLess').textContent, 'read less', "read more/less link should contain 'read less' as text after it has been clicked" ); }); QUnit.test('Form view not scrolled when switching record', async function (assert) { assert.expect(6); this.data['res.partner'].records.push( { id: 11, display_name: "Partner 1", description: [...Array(60).keys()].join('\n'), }, { id: 12, display_name: "Partner 2", } ); const messages = [...Array(60).keys()].map(id => { return { model: 'res.partner', res_id: id % 2 ? 11 : 12, }; }); this.data['mail.message'].records = messages; await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', arch: `
`, viewOptions: { currentId: 11, ids: [11, 12], }, config: { device: { size_class: config.device.SIZES.LG }, }, env: { device: { size_class: config.device.SIZES.LG }, }, }); const controllerContentEl = document.querySelector('.o_content'); assert.strictEqual( document.querySelector('.breadcrumb-item.active').textContent, 'Partner 1', "Form view should display partner 'Partner 1'" ); assert.strictEqual(controllerContentEl.scrollTop, 0, "The top of the form view is visible" ); await afterNextRender(async () => { controllerContentEl.scrollTop = controllerContentEl.scrollHeight - controllerContentEl.clientHeight; await triggerEvent( document.querySelector('.o_ThreadView_messageList'), 'scroll' ); }); assert.strictEqual( controllerContentEl.scrollTop, controllerContentEl.scrollHeight - controllerContentEl.clientHeight, "The controller container should be scrolled to its bottom" ); await afterNextRender(() => document.querySelector('.o_pager_next').click() ); assert.strictEqual( document.querySelector('.breadcrumb-item.active').textContent, 'Partner 2', "The form view should display partner 'Partner 2'" ); assert.strictEqual(controllerContentEl.scrollTop, 0, "The top of the form view should be visible when switching record from pager" ); await afterNextRender(() => document.querySelector('.o_pager_previous').click() ); assert.strictEqual(controllerContentEl.scrollTop, 0, "Form view's scroll position should have been reset when switching back to first record" ); }); QUnit.test('Attachments that have been unlinked from server should be visually unlinked from record', async function (assert) { // Attachments that have been fetched from a record at certain time and then // removed from the server should be reflected on the UI when the current // partner accesses this record again. assert.expect(2); this.data['res.partner'].records.push( { display_name: "Partner1", id: 11 }, { display_name: "Partner2", id: 12 } ); this.data['ir.attachment'].records.push( { id: 11, mimetype: 'text.txt', res_id: 11, res_model: 'res.partner', }, { id: 12, mimetype: 'text.txt', res_id: 11, res_model: 'res.partner', } ); await this.createView({ data: this.data, hasView: true, // View params View: FormView, model: 'res.partner', res_id: 11, viewOptions: { ids: [11, 12], index: 0, }, arch: `
`, }); assert.strictEqual( document.querySelector('.o_ChatterTopbar_buttonCount').textContent, '2', "Partner1 should have 2 attachments initially" ); // The attachment links are updated on (re)load, // so using pager is a way to reload the record "Partner1". await afterNextRender(() => document.querySelector('.o_pager_next').click() ); // Simulate unlinking attachment 12 from Partner 1. this.data['ir.attachment'].records.find(a => a.id === 11).res_id = 0; await afterNextRender(() => document.querySelector('.o_pager_previous').click() ); assert.strictEqual( document.querySelector('.o_ChatterTopbar_buttonCount').textContent, '1', "Partner1 should now have 1 attachment after it has been unlinked from server" ); }); QUnit.test('chatter just contains "creating a new record" message during the creation of a new record after having displayed a chatter for an existing record', async function (assert) { assert.expect(2); this.data['res.partner'].records.push({ id: 12 }); await this.createView({ data: this.data, hasView: true, View: FormView, model: 'res.partner', res_id: 12, arch: `
`, }); await afterNextRender(() => { document.querySelector('.o_form_button_create').click(); }); assert.containsOnce( document.body, '.o_Message', "Should have a single message when creating a new record" ); assert.strictEqual( document.querySelector('.o_Message_content').textContent, 'Creating a new record...', "the message content should be in accord to the creation of this record" ); }); }); }); }); });