/* @odoo-module */ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; import { ActivityController } from "@mail/views/web/activity/activity_controller"; import { ActivityModel } from "@mail/views/web/activity/activity_model"; import { ActivityRenderer } from "@mail/views/web/activity/activity_renderer"; import { DynamicList } from "@web/model/relational_model/dynamic_list" import { start } from "@mail/../tests/helpers/test_utils"; import { RelationalModel } from "@web/model/relational_model/relational_model"; import { Domain } from "@web/core/domain"; import { serializeDate, formatDate } from "@web/core/l10n/dates"; import { deepEqual, omit } from "@web/core/utils/objects"; import { session } from "@web/session"; import testUtils from "@web/../tests/legacy/helpers/test_utils"; import { editInput, patchWithCleanup, click, patchDate, triggerEvent } from "@web/../tests/helpers/utils"; import { toggleSearchBarMenu } from "@web/../tests/search/helpers"; import { contains, insertText } from "@web/../tests/utils"; import { doAction } from "@web/../tests/webclient/helpers"; import { onMounted, onWillUnmount } from "@odoo/owl"; const { DateTime } = luxon; let serverData; let pyEnv; async function openViewAndPatchDoAction(assert) { const { env, openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); patchWithCleanup(env.services.action, { doAction(action, options) { assert.step("doAction"); options.onClose(); }, }); } function patchActivityDomain(load, params) { if (params.domain) { // Remove domain term used to filter record having "done" activities (not understood by the getRecords mock) const domain = new Domain(params.domain); const newDomain = Domain.removeDomainLeaves(domain.toList(), [ "activity_ids.active", ]); if (!deepEqual(domain.toList(), newDomain.toList())) { return load({ ...params, domain: newDomain.toList(), context: params.context ? { ...params.context, active_test: false } : { active_test: false }, }); } } return load(params); } QUnit.module("test_mail", {}, function () { QUnit.module("activity view", { async beforeEach() { patchDate(2023, 4, 8, 10, 0, 0); patchWithCleanup(DynamicList.prototype, { async load(params) { return patchActivityDomain(super.load.bind(this), params); }, }) patchWithCleanup(RelationalModel.prototype, { async load(params) { return patchActivityDomain(super.load.bind(this), params); }, }); pyEnv = await startServer(); const mailTemplateIds = pyEnv["mail.template"].create([ { name: "Template1" }, { name: "Template2" }, ]); // reset incompatible setup pyEnv["mail.activity.type"].unlink(pyEnv["mail.activity.type"].search([])); const mailActivityTypeIds = pyEnv["mail.activity.type"].create([ { name: "Email", mail_template_ids: mailTemplateIds }, { name: "Call" }, { name: "Call for Demo" }, { name: "To Do" }, ]); const resUsersId1 = pyEnv["res.users"].create({ display_name: "first user" }); const mailActivityIds = pyEnv["mail.activity"].create([ { display_name: "An activity", date_deadline: serializeDate(DateTime.now().plus({ days: 3 })), can_write: true, state: "planned", activity_type_id: mailActivityTypeIds[0], mail_template_ids: mailTemplateIds, user_id: resUsersId1, }, { display_name: "An activity", date_deadline: serializeDate(DateTime.now()), can_write: true, state: "today", activity_type_id: mailActivityTypeIds[0], mail_template_ids: mailTemplateIds, user_id: resUsersId1, }, { res_model: "mail.test.activity", display_name: "An activity", date_deadline: serializeDate(DateTime.now().minus({ days: 2 })), can_write: true, state: "overdue", activity_type_id: mailActivityTypeIds[1], user_id: resUsersId1, }, ]); pyEnv["mail.test.activity"].create([ { name: "Meeting Room Furnitures", activity_ids: [mailActivityIds[0]] }, { name: "Office planning", activity_ids: [mailActivityIds[1], mailActivityIds[2]] }, ]); serverData = { views: { "mail.test.activity,false,activity": '' + "" + '
' + '' + "
" + "
" + "
", "mail.test.activity,1,activity": `
Test invisible
`, }, }; }, }); QUnit.test("activity view: simple activity rendering", async function (assert) { assert.expect(14); const mailTestActivityIds = pyEnv["mail.test.activity"].search([]); const mailActivityTypeIds = pyEnv["mail.activity.type"].search([]); const { env, openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); patchWithCleanup(env.services.action, { doAction(action, options) { assert.deepEqual( action, { context: { default_res_id: mailTestActivityIds[1], default_res_model: "mail.test.activity", default_activity_type_id: mailActivityTypeIds[2], }, res_id: false, res_model: "mail.activity", target: "new", type: "ir.actions.act_window", view_mode: "form", view_type: "form", views: [[false, "form"]], }, "should do a do_action with correct parameters" ); options.onClose(); return Promise.resolve(); }, }); const $activity = $(document.querySelector(".o_activity_view")); assert.containsOnce($activity, "table", "should have a table"); const $th1 = $activity.find("table thead tr:first th:nth-child(2)"); assert.containsOnce( $th1, "span:first:contains(Email)", 'should contain "Email" in header of first column' ); assert.containsOnce( $th1, ".o_activity_counter", "should contain a progressbar in header of first column" ); assert.hasAttrValue( $th1.find(".o_column_progress .progress-bar:first"), "data-tooltip", "1 Planned", "the counter progressbars should be correctly displayed" ); assert.hasAttrValue( $th1.find(".o_column_progress .progress-bar:nth-child(2)"), "data-tooltip", "1 Today", "the counter progressbars should be correctly displayed" ); const $th2 = $activity.find("table thead tr:first th:nth-child(3)"); assert.containsOnce( $th2, "span:first:contains(Call)", 'should contain "Call" in header of second column' ); assert.hasAttrValue( $th2.find(".o_column_progress .progress-bar"), "data-tooltip", "1 Overdue", "the counter progressbars should be correctly displayed" ); assert.containsNone( $activity, "table thead tr:first th:nth-child(4) .o_kanban_counter", "should not contain a progressbar in header of 3rd column" ); assert.ok( $activity.find("table tbody tr:first td:first:contains(Office planning)").length, 'should contain "Office planning" in first colum of first row' ); assert.ok( $activity.find("table tbody tr:nth-child(2) td:first:contains(Meeting Room Furnitures)") .length, 'should contain "Meeting Room Furnitures" in first colum of second row' ); const today = formatDate(DateTime.now()); assert.ok( $activity.find( "table tbody tr:first td:nth-child(2).today .o-mail-ActivityCell-deadline:contains(" + today + ")" ).length, "should contain an activity for today in second cell of first line " + today ); const td = "table tbody tr:nth-child(1) td.o_activity_empty_cell"; assert.containsN( $activity, td, 2, "should contain an empty cell as no activity scheduled yet." ); // schedule an activity (this triggers a do_action) await testUtils.fields.editAndTrigger($activity.find(td + ":first"), null, [ "mouseenter", "click", ]); assert.containsOnce( $activity, "table tfoot tr .o_record_selector", "should contain search more selector to choose the record to schedule an activity for it" ); }); QUnit.test("activity view: Activity rendering with done activities", async function (assert) { const activityTypeUpload = pyEnv["mail.activity.type"].create({ category: "upload_file", name: "Test Upload document", keep_done: true, }); pyEnv["mail.activity"].create( Object.entries(["done", "done", "done", "done", "planned", "planned", "planned"]).map( ([idx, state]) => ({ active: state !== "done", activity_type_id: activityTypeUpload, attachment_ids: state === "done" ? [ pyEnv["ir.attachment"].create({ name: `attachment ${idx}`, create_date: serializeDate( DateTime.now().minus({ days: idx }) ), create_uid: pyEnv.currentUserId, }), ] : [], can_write: true, date_deadline: serializeDate(DateTime.now().plus({ days: idx })), date_done: state === "done" ? serializeDate(DateTime.now().minus({ days: idx })) : false, display_name: `Upload folders ${idx}`, state: state, user_id: pyEnv["res.users"].create({ display_name: `user${idx}` }), }) ) ); const [meetingRecord, officeRecord] = pyEnv["mail.test.activity"].search([]); const uploadDoneActs = pyEnv["mail.activity"].searchRead([ ["activity_type_id", "=", activityTypeUpload], ["active", "=", false], ]); const uploadPlannedActs = pyEnv["mail.activity"].searchRead([ ["activity_type_id", "=", activityTypeUpload], ]); pyEnv["mail.test.activity"].write([meetingRecord], { activity_ids: [ uploadPlannedActs[0].id, uploadPlannedActs[1].id, uploadPlannedActs[2].id, uploadDoneActs[0].id, ], }); pyEnv["mail.test.activity"].write([officeRecord], { activity_ids: [uploadDoneActs[1].id, uploadDoneActs[2].id, uploadDoneActs[3].id], }); const { openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); const domActivity = document.querySelector(".o_activity_view"); const domHeaderUpload = domActivity.querySelector( "table thead tr:first-child th:nth-child(6)" ); const selRowMeetingCellUpload = "table tbody tr:first-child td:nth-child(6)"; const domRowMeetingCellUpload = domActivity.querySelector(selRowMeetingCellUpload); const selRowOfficeCellUpload = "table tbody tr:nth-child(2) td:nth-child(6)"; const domRowOfficeCellUpload = domActivity.querySelector(selRowOfficeCellUpload); // Headers await contains(".o_column_progress .progress-bar:first-child[data-tooltip='3 Planned']", { target: domHeaderUpload, }); await contains(".o_animated_number", { target: domHeaderUpload, text: "3", }); await contains(".o_column_progress_aggregated_on", { target: domHeaderUpload, text: "7", }); // Cells avatars await contains( `.o-mail-Avatar img[data-src='/web/image/res.users/${uploadPlannedActs[0].user_id[0]}/avatar_128'`, { target: domRowMeetingCellUpload } ); await contains( `.o-mail-Avatar img[data-src='/web/image/res.users/${uploadPlannedActs[1].user_id[0]}/avatar_128'`, { target: domRowMeetingCellUpload } ); await contains( `.o-mail-Avatar img[data-src='/web/image/res.users/${uploadPlannedActs[2].user_id[0]}/avatar_128'`, { target: domRowMeetingCellUpload, count: 0 } ); await contains(`.o-mail-Avatar`, { target: domRowOfficeCellUpload, count: 0 }); // all activity are done // Cells counters await contains(".o-mail-ActivityCell-counter", { target: domRowMeetingCellUpload, text: "3 / 4", }); await contains(".o-mail-ActivityCell-counter", { text: "3", target: domRowOfficeCellUpload, }); // Cells dates await contains(".o-mail-ActivityCell-deadline", { text: formatDate(luxon.DateTime.fromISO(uploadPlannedActs[0].date_deadline)), target: domRowMeetingCellUpload, }); await contains(".o-mail-ActivityCell-deadline", { text: formatDate(luxon.DateTime.fromISO(uploadDoneActs[1].date_done)), target: domRowOfficeCellUpload, }); // Activity list popovers content await click(domActivity, `${selRowMeetingCellUpload} > div`); await contains(".o-mail-ActivityListPopover .badge.text-bg-success", { text: "3" }); // 3 planned for (const actIdx of [0, 1, 2]) { await contains(".o-mail-ActivityListPopoverItem", { text: uploadPlannedActs[actIdx].user_id[1], }); } await contains(".o-mail-ActivityListPopoverItem", { text: "Due in 4 days" }); await contains(".o-mail-ActivityListPopoverItem", { text: "Due in 5 days" }); await contains(".o-mail-ActivityListPopoverItem", { text: "Due in 6 days" }); await contains(".o-mail-ActivityListPopover .badge.text-bg-secondary", { text: "1" }); // 1 done await contains(".o-mail-ActivityListPopoverItem", { text: uploadDoneActs[0].user_id[1] }); await contains(".o-mail-ActivityListPopoverItem", { text: formatDate(luxon.DateTime.fromISO(uploadDoneActs[0].date_done), {format: "M/d/yyyy"}) }); await click(domActivity, `${selRowOfficeCellUpload} > div`); await contains(".o-mail-ActivityListPopover .badge.text-bg-secondary", { text: "3" }); // 3 done for (const actIdx of [1, 2, 3]) { console.log(); await contains(".o-mail-ActivityListPopoverItem", { text: formatDate(luxon.DateTime.fromISO(uploadDoneActs[actIdx].date_done), {format: "M/d/yyyy"}), }); await contains(".o-mail-ActivityListPopoverItem", { text: uploadDoneActs[actIdx].user_id[1], }); } }); QUnit.test( "activity view: a pager can be used when there are more than the limit of 100 activities to display", async function (assert) { const mailActivityTypeIds = pyEnv["mail.activity.type"].search([]); const recordsToCreate = []; const activityToCreate = []; for (let i = 0; i < 101; i++) { activityToCreate.push({ display_name: "An activity " + i * 2, date_deadline: serializeDate(DateTime.now().plus({ days: 3 })), can_write: true, state: "planned", activity_type_id: mailActivityTypeIds[0], }); activityToCreate.push({ display_name: "An activity " + (i * 2 + 1), date_deadline: serializeDate(DateTime.now().plus({ days: 2 })), can_write: true, state: "planned", activity_type_id: mailActivityTypeIds[1], }); } const createdActivity = pyEnv["mail.activity"].create(activityToCreate); for (let i = 0; i < 101; i++) { recordsToCreate.push({ name: "pagerTestRecord" + i, activity_ids: [createdActivity[i * 2], createdActivity[i * 2 + 1]], }); } pyEnv["mail.test.activity"].create(recordsToCreate); const { openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], domain: [["name", "like", "pagerTestRecord"]], }); assert.containsN( document.body, ".o_activity_record", 100, "Only 100 records should have been displayed" ); assert.containsN( document.body, ".o_activity_summary_cell.planned", 200, "200 activities should have been displayed (2 per records)" ); await click(document.querySelector(".o_pager_next")); assert.containsN( document.body, ".o_activity_record", 1, "Only 1 record is now displayed" ); assert.containsN( document.body, ".o_activity_summary_cell.planned", 2, "Only the 2 activities of the last record are now displayed" ); await click(document.querySelector(".o_pager_previous")); assert.containsN(document.body, ".o_activity_record", 100); assert.containsN(document.body, ".o_activity_summary_cell.planned", 200); } ); QUnit.test("activity view: no content rendering", async function () { const { openView, pyEnv } = await start({ serverData }); // reset incompatible setup pyEnv["mail.activity.type"].unlink(pyEnv["mail.activity.type"].search([])); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); await contains(".o_view_nocontent"); await contains(".o_view_nocontent .o_view_nocontent_empty_folder", { text: "No data to display", }); }); QUnit.test("activity view: batch send mail on activity", async function (assert) { assert.expect(6); const mailTestActivityIds = pyEnv["mail.test.activity"].search([]); const mailTemplateIds = pyEnv["mail.template"].search([]); const { openView } = await start({ serverData, mockRPC: function (route, args) { if (args.method === "activity_send_mail") { assert.step(JSON.stringify(args.args)); return Promise.resolve(true); } }, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); const $activity = $(document); assert.notOk( $activity.find( "table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show" ).length, "dropdown shouldn't be displayed" ); testUtils.dom.click( $activity.find("table thead tr:first th:nth-child(2) span:nth-child(2) i.fa-ellipsis-v") ); assert.ok( $activity.find( "table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show" ).length, "dropdown should have appeared" ); testUtils.dom.click( $activity.find( "table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show .o_send_mail_template:contains(Template2)" ) ); assert.notOk( $activity.find( "table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show" ).length, "dropdown shouldn't be displayed" ); testUtils.dom.click( $activity.find("table thead tr:first th:nth-child(2) span:nth-child(2) i.fa-ellipsis-v") ); testUtils.dom.click( $activity.find( "table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show .o_send_mail_template:contains(Template1)" ) ); assert.verifySteps([ `[[${mailTestActivityIds[0]},${mailTestActivityIds[1]}],${mailTemplateIds[1]}]`, // send mail template 1 on mail.test.activity 1 and 2 `[[${mailTestActivityIds[0]},${mailTestActivityIds[1]}],${mailTemplateIds[0]}]`, // send mail template 2 on mail.test.activity 1 and 2 ]); }); QUnit.test("activity view: activity_ids condition in domain", async function (assert) { assert.expect(5); const { openView, target } = await start({ serverData, mockRPC: function (route, args) { if (["get_activity_data", "web_search_read"].includes(args.method)) { assert.step(JSON.stringify(args.kwargs.domain)); } }, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); // enter edit mode await click(target, ".o_pager_value"); await triggerEvent(target, ".o_pager_value", 'keydown', { key: 'Enter' }); assert.verifySteps([ // load view requests JSON.stringify([["activity_ids.active", "in", [true, false]]]), '[[1,"=",1]]', // Due to the relational model patch above that removes it // pager requests JSON.stringify([["activity_ids.active", "in", [true, false]]]), // Due to the dynamic list patch above that removes it '[[1,"=",1]]', ]); }); QUnit.test("activity view: activity widget", async function (assert) { assert.expect(16); const mailActivityTypeIds = pyEnv["mail.activity.type"].search([]); const [mailTestActivityId2] = pyEnv["mail.test.activity"].search([ ["name", "=", "Office planning"], ]); const [mailTemplateId1] = pyEnv["mail.template"].search([["name", "=", "Template1"]]); const { env, openView } = await start({ mockRPC: function (route, args) { if (args.method === "activity_send_mail") { assert.deepEqual( [[mailTestActivityId2], mailTemplateId1], args.args, "Should send template related to mailTestActivity2" ); assert.step("activity_send_mail"); // random value returned in order for the mock server to know that this route is implemented. return true; } if (args.method === "action_feedback_schedule_next") { assert.deepEqual( [pyEnv["mail.activity"].search([["state", "=", "overdue"]])], args.args, "Should execute action_feedback_schedule_next only on the overude activity" ); assert.equal(args.kwargs.feedback, "feedback2"); assert.step("action_feedback_schedule_next"); return Promise.resolve({ serverGeneratedAction: true }); } }, serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); patchWithCleanup(env.services.action, { doAction(action) { if (action.serverGeneratedAction) { assert.step("serverGeneratedAction"); } else if (action.res_model === "mail.compose.message") { assert.deepEqual( { default_model: "mail.test.activity", default_res_ids: [mailTestActivityId2], default_subtype_xmlid: "mail.mt_comment", default_template_id: mailTemplateId1, force_email: true, }, action.context ); assert.step("do_action_compose"); } else if (action.res_model === "mail.activity.schedule") { assert.deepEqual( { default_activity_type_id: mailActivityTypeIds[1], active_id: mailTestActivityId2, active_ids: [mailTestActivityId2], active_model: "mail.test.activity", }, action.context ); assert.step("do_action_activity"); } else { assert.step("Unexpected action" + action.res_model); } return Promise.resolve(); }, }); await click(document.querySelector(".today .o-mail-ActivityCell-deadline")); assert.containsOnce( document.body, ".o-mail-ActivityListPopover", "dropdown should be displayed" ); assert.ok( document .querySelector(".o-mail-ActivityListPopover-todayTitle") .textContent.includes("Today"), "Title should be today" ); assert.ok( [...document.querySelectorAll(".o-mail-ActivityMailTemplate-name")].filter((el) => el.textContent.includes("Template1") ).length, "Template1 should be available" ); assert.ok( [...document.querySelectorAll(".o-mail-ActivityMailTemplate-name")].filter((el) => el.textContent.includes("Template2") ).length, "Template2 should be available" ); await click(document.querySelector(".o-mail-ActivityMailTemplate-preview")); await click(document.querySelector(".today .o-mail-ActivityCell-deadline")); await click(document.querySelector(".o-mail-ActivityMailTemplate-send")); await click(document.querySelector(".overdue .o-mail-ActivityCell-deadline")); assert.containsNone( document.body, ".o-mail-ActivityMailTemplate-name", "No template should be available" ); await click($(".o-mail-ActivityListPopover button:contains(Schedule an activity)")[0]); await click(document.querySelector(".overdue .o-mail-ActivityCell-deadline")); await click(document.querySelector(".o-mail-ActivityListPopoverItem-markAsDone")); await editInput( document.body, ".o-mail-ActivityMarkAsDone textarea[placeholder='Write Feedback']", "feedback2" ); await click( document.querySelector( ".o-mail-ActivityMarkAsDone button[aria-label='Done and Schedule Next']" ) ); assert.verifySteps([ "do_action_compose", "activity_send_mail", "do_action_activity", "action_feedback_schedule_next", "serverGeneratedAction", ]); }); QUnit.test("activity view: Mark as done with keep done enabled", async function (assert) { const emailActType = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0]; pyEnv["mail.activity.type"].write([emailActType], { keep_done: true, }); const { openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], context: { active_test: false }, }); const domActivity = document.querySelector(".o_activity_view"); const domHeaderEmail = domActivity.querySelector( "table thead tr:first-child th:nth-child(2)" ); const selRowOfficeCellEmail = "table tbody tr:nth-child(2) td:nth-child(2)"; await contains(".o_animated_number", { target: domHeaderEmail, text: "2", }); await contains(".o_column_progress_aggregated_on", { target: domHeaderEmail, text: "2", }); await click(domActivity, `${selRowOfficeCellEmail} > div`); await click( document, ".o-mail-ActivityListPopoverItem .o-mail-ActivityListPopoverItem-markAsDone" ); await click(document, ".o-mail-ActivityMarkAsDone button[aria-label='Done']"); await contains(".o_animated_number", { target: domHeaderEmail, text: "1", }); await contains(".o_column_progress_aggregated_on", { target: domHeaderEmail, text: "2", }); }); QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) { assert.expect(4); serverData.actions = { 1: { id: 1, name: "MailTestActivity Action", res_model: "mail.test.activity", type: "ir.actions.act_window", views: [[false, "activity"]], }, }; const mockRPC = (route, args) => { if (args.method === "get_activity_data") { assert.strictEqual( args.kwargs.context.lang, "zz_ZZ", "The context should have been passed" ); } }; patchWithCleanup(session.user_context, { lang: "zz_ZZ" }); const { webClient } = await start({ serverData, mockRPC }); await doAction(webClient, 1); await toggleSearchBarMenu(document); assert.containsN( document.body, ".o_cp_searchview .o_dropdown_container", 2, "only two elements should be available in view search" ); assert.isVisible( document.querySelector(".o_cp_searchview .o_dropdown_container.o_filter_menu"), "filter should be available in view search" ); assert.isVisible( document.querySelector(".o_cp_searchview .o_dropdown_container.o_favorite_menu"), "favorites should be available in view search" ); }); QUnit.test("activity view: group_by in the action has no effect", async function (assert) { assert.expect(1); patchWithCleanup(ActivityModel.prototype, { async load(params) { // force params to have a groupBy set, the model should ignore this value during the load params.groupBy = ["user_id"]; await super.load(params); }, }); serverData.actions = { 1: { id: 1, name: "MailTestActivity Action", res_model: "mail.test.activity", type: "ir.actions.act_window", views: [[false, "activity"]], }, }; const mockRPC = (route, args) => { if (args.method === "get_activity_data") { assert.strictEqual( args.kwargs.groupby, undefined, "groupby should have been removed from the load params" ); } }; const { webClient } = await start({ serverData, mockRPC }); await doAction(webClient, 1); }); QUnit.test( "activity view: search more to schedule an activity for a record of a respecting model", async function (assert) { assert.expect(5); const mailTestActivityId1 = pyEnv["mail.test.activity"].create({ name: "MailTestActivity 3", }); Object.assign(serverData.views, { "mail.test.activity,false,list": '', }); const { env, openView } = await start({ mockRPC(route, args) { if (args.method === "name_search") { args.kwargs.name = "MailTestActivity"; } }, serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); patchWithCleanup(env.services.action, { doAction(action, options) { assert.step("doAction"); const expectedAction = { context: { active_ids: [mailTestActivityId1], active_id: mailTestActivityId1, active_model: "mail.test.activity", }, name: "Schedule Activity", res_model: "mail.activity.schedule", target: "new", type: "ir.actions.act_window", view_mode: "form", views: [[false, "form"]], }; assert.deepEqual( action, expectedAction, "should execute an action with correct params" ); options.onClose(); return Promise.resolve(); }, }); const activity = $(document); assert.containsOnce( activity, "table tfoot tr .o_record_selector", "should contain search more selector to choose the record to schedule an activity for it" ); await testUtils.dom.click(activity.find("table tfoot tr .o_record_selector")); // search create dialog const $modal = $(".modal-lg"); assert.strictEqual( $modal.find(".o_data_row").length, 3, "all mail.test.activity should be available to select" ); // select a record to schedule an activity for it (this triggers a do_action) await testUtils.dom.click($modal.find(".o_data_row:last .o_data_cell")); assert.verifySteps(["doAction"]); } ); QUnit.test("activity view: Domain should not reset on load", async function (assert) { Object.assign(serverData.views, { "mail.test.activity,false,list": '', }); const { env, openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], domain: [['id', '=', 1]], }); patchWithCleanup(env.services.action, { doAction(action, options) { assert.step("doAction"); options.onClose(); }, }); await click(document.querySelector(".o_activity_view .o_record_selector")); // search create dialog await click(document.querySelector(".modal-lg .o_data_row .o_data_cell")); assert.verifySteps(["doAction"]); await click(document.querySelector(".o_activity_view .o_record_selector")); // again open search create dialog assert.strictEqual( document.querySelectorAll(".modal-lg .o_data_row").length, 1, "Should contains only one record after calling schedule activity which load view again" ); }); QUnit.test( "activity view: 'scheduleActivity' does not add activity_ids condition as selectCreateDialog domain", async function (assert) { patchWithCleanup(ActivityController.prototype, { scheduleActivity() { super.scheduleActivity(); assert.step(JSON.stringify(this.getSearchProps().domain)); }, }); Object.assign(serverData.views, { "mail.test.activity,false,list": '', }); await openViewAndPatchDoAction(assert); // open search create dialog and schedule an activity await click(document.querySelector(".o_activity_view .o_record_selector")); await click(document.querySelectorAll(".modal-lg .o_data_row .o_data_cell")[0]); // again open search create dialog await click(document.querySelector(".o_activity_view .o_record_selector")); assert.verifySteps(["[]", "doAction", "[]"]); } ); QUnit.test( "activity view: 'onClose' of 'openActivityFormView' does not add activity_ids condition as selectCreateDialog domain", async function (assert) { patchWithCleanup(ActivityController.prototype, { openActivityFormView(resId, activityTypeId) { super.openActivityFormView(resId, activityTypeId); assert.step(JSON.stringify(this.getSearchProps().domain)); }, }); await openViewAndPatchDoAction(assert); //schedule an activity on an empty activity cell await click( document.querySelector(".o_activity_view .o_data_row .o_activity_empty_cell") ); assert.verifySteps(["doAction", "[]"]); } ); QUnit.test( "activity view: 'onReloadData' does not add activity_ids condition as selectCreateDialog domain", async function (assert) { patchWithCleanup(ActivityController.prototype, { get rendererProps() { const rendererProps = { ...super.rendererProps }; assert.step(JSON.stringify(this.getSearchProps().domain)); return rendererProps; }, }); await openViewAndPatchDoAction(assert); //schedule another activity on an activity cell with a scheduled activity await click(document.querySelector(".today .o-mail-ActivityCell-deadline")); await click($(".o-mail-ActivityListPopover button:contains(Schedule an activity)")[0]); assert.verifySteps(["[]", "doAction", "[]", "[]"]); } ); QUnit.test("Activity view: discard an activity creation dialog", async function (assert) { assert.expect(2); serverData.actions = { 1: { id: 1, name: "MailTestActivity Action", res_model: "mail.test.activity", type: "ir.actions.act_window", views: [[false, "activity"]], }, }; Object.assign(serverData.views, { "mail.activity,false,form": `
`, }); const mockRPC = (route, args) => { if (args.method === "check_access_rights") { return true; } }; const { webClient } = await start({ serverData, mockRPC }); await doAction(webClient, 1); await testUtils.dom.click( document.querySelector(".o_activity_view .o_data_row .o_activity_empty_cell") ); await contains(".modal.o_technical_modal"); await testUtils.dom.click($('.modal.o_technical_modal button[special="cancel"]')); await contains(".modal.o_technical_modal", { count: 0 }); }); QUnit.test( "Activity view: many2one_avatar_user widget in activity view", async function (assert) { assert.expect(3); const [mailTestActivityId1] = pyEnv["mail.test.activity"].search([ ["name", "=", "Meeting Room Furnitures"], ]); const resUsersId1 = pyEnv["res.users"].create({ display_name: "first user", avatar_128: "Atmaram Bhide", }); pyEnv["mail.test.activity"].write([mailTestActivityId1], { activity_user_id: resUsersId1, }); Object.assign(serverData.views, { "mail.test.activity,false,activity": `
`, }); serverData.actions = { 1: { id: 1, name: "MailTestActivity Action", res_model: "mail.test.activity", type: "ir.actions.act_window", views: [[false, "activity"]], }, }; const { webClient } = await start({ serverData }); await doAction(webClient, 1); await contains(".o_m2o_avatar", { count: 2 }); assert.containsOnce( document.body, `tr:nth-child(2) .o_m2o_avatar > img[data-src="/web/image/res.users/${resUsersId1}/avatar_128"]`, "should have m2o avatar image" ); // "should not have text on many2one_avatar_user if onlyImage node option is passed" await contains(".o_m2o_avatar > span", { count: 0 }); } ); QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (assert) { assert.expect(3); patchWithCleanup(ActivityRenderer.prototype, { setup() { super.setup(); onMounted(() => { assert.step("mounted"); }); onWillUnmount(() => { assert.step("willUnmount"); }); }, }); const { openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); // force the unmounting of the activity view by opening another one await openView({ res_model: "mail.test.activity", views: [[false, "form"]], }); assert.verifySteps(["mounted", "willUnmount"]); }); QUnit.test( "Schedule activity dialog uses the same search view as activity view", async function (assert) { assert.expect(8); pyEnv["mail.test.activity"].unlink(pyEnv["mail.test.activity"].search([])); Object.assign(serverData.views, { "mail.test.activity,false,list": ``, "mail.test.activity,false,search": ``, "mail.test.activity,1,search": ``, }); function mockRPC(route, args) { if (args.method === "get_views") { assert.step(JSON.stringify(args.kwargs.views)); } } const { webClient } = await start({ serverData, mockRPC }); // open an activity view (with default search arch) await doAction(webClient, { name: "Dashboard", res_model: "mail.test.activity", type: "ir.actions.act_window", views: [[false, "activity"]], }); assert.verifySteps(['[[false,"activity"],[false,"search"]]']); // click on "Schedule activity" await click(document.querySelector(".o_activity_view .o_record_selector")); assert.verifySteps(['[[false,"list"],[false,"search"]]']); // open an activity view (with search arch 1) await doAction(webClient, { name: "Dashboard", res_model: "mail.test.activity", type: "ir.actions.act_window", views: [[false, "activity"]], search_view_id: [1, "search"], }); assert.verifySteps(['[[false,"activity"],[1,"search"]]']); // click on "Schedule activity" await click(document.querySelector(".o_activity_view .o_record_selector")); assert.verifySteps(['[[false,"list"],[1,"search"]]']); } ); QUnit.test("Activity view: apply progressbar filter", async function (assert) { assert.expect(12); const mailActivityTypeIds = pyEnv["mail.activity.type"].search([]); const mailTemplateIds = pyEnv["mail.template"].search([]); const [resUsersId1] = pyEnv["res.users"].search([]); pyEnv["mail.activity"].create([ { display_name: "An activity", date_deadline: serializeDate(DateTime.now().plus({ days: 3 })), can_write: true, state: "planned", activity_type_id: mailActivityTypeIds[2], mail_template_ids: mailTemplateIds, user_id: resUsersId1, }, ]); const mailActivityIds = pyEnv["mail.activity"].search([]); const [mailTestActivityId1] = pyEnv["mail.test.activity"].search([ ["name", "=", "Meeting Room Furnitures"], ]); pyEnv["mail.test.activity"].write([mailTestActivityId1], { activity_ids: [mailActivityIds[0], mailActivityIds[3]], }); serverData.actions = { 1: { id: 1, name: "MailTestActivity Action", res_model: "mail.test.activity", type: "ir.actions.act_window", views: [[false, "activity"]], }, }; const { target, webClient } = await start({ serverData }); await doAction(webClient, 1); assert.containsNone( document.querySelector(".o_activity_view thead"), ".o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false", "should not have active filter" ); assert.containsNone( document.querySelector(".o_activity_view tbody"), ".o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false", "should not have active filter" ); assert.strictEqual( document.querySelector(".o_activity_view tbody .o_activity_record").textContent, "Office planning", "'Office planning' should be first record" ); assert.containsN( document.querySelector(".o_activity_view tbody"), ".planned", 2, "other records should be available" ); await click(document.querySelector(".o_column_progress .progress-bar")); assert.containsOnce( document.querySelector(".o_activity_view thead"), ".o_activity_filter_planned", "planned should be active filter" ); assert.hasClass( target.querySelector(".o_activity_type_cell:nth-child(2) .bg-success"), "progress-bar-animated progress-bar-striped", "progress bar is animated with a strip effect" ); assert.containsOnce(target, ".progress-bar-striped", "only one progress bar is animated"); assert.containsN( document.querySelector(".o_activity_view tbody"), ".o_activity_filter_planned", 5, "planned should be active filter" ); assert.containsNone( document.querySelector(".o_activity_view thead tr :nth-child(4)"), ".progress-bar-animated", "the progress bar of the Call for Demo activity type should not be animated" ); assert.strictEqual( document.querySelector(".o_activity_view tbody .o_activity_record").textContent, "Meeting Room Furnitures", "'Office planning' should be first record" ); const tr = document.querySelectorAll(".o_activity_view tbody tr")[1]; assert.hasClass( tr.querySelectorAll("td")[1], "o_activity_empty_cell", "other records should be hidden" ); assert.containsNone( document.querySelector(".o_activity_view tbody"), "planned", "other records should be hidden" ); }); QUnit.test("Activity view: hide/show columns", async function (assert) { const { openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); let expectedColumns = ["Email", "Call", "Call for Demo", "To Do"]; for (const [index, column] of expectedColumns.entries()) { assert.strictEqual( document.querySelectorAll(".o_activity_view th div span:first-child")[index] .textContent, column, "The column names should match" ); } assert.containsOnce( document.body, "th:last-child button.dropdown-toggle", "The last column is the column selector" ); await click(document.querySelector("th:last-child button.dropdown-toggle")); await click(document.body, "input[name='Email']"); expectedColumns = ["Call", "Call for Demo", "To Do"]; for (const [index, column] of expectedColumns.entries()) { assert.strictEqual( document.querySelectorAll(".o_activity_view th div span:first-child")[index] .textContent, column, "The column names should match" ); } await click(document.body, "input[name='Call for Demo']"); expectedColumns = ["Call", "To Do"]; for (const [index, column] of expectedColumns.entries()) { assert.strictEqual( document.querySelectorAll(".o_activity_view th div span:first-child")[index] .textContent, column, "The column names should match" ); } await click(document.body, "input[name='Email']"); expectedColumns = ["Email", "Call", "To Do"]; for (const [index, column] of expectedColumns.entries()) { assert.strictEqual( document.querySelectorAll(".o_activity_view th div span:first-child")[index] .textContent, column, "The column names should match" ); } }); QUnit.test("Activity view: luxon in renderingContext", async function (assert) { Object.assign(serverData.views, { "mail.test.activity,false,activity": `
luxon
`, }); const { openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); await contains(".luxon", { count: 2 }); }); QUnit.test("test displaying image (write_date field)", async (assert) => { // the presence of write_date field ensures that the image is reloaded when necessary assert.expect(2); Object.assign(serverData.views, { "mail.test.activity,false,activity": `
`, }); const { target, openView } = await start({ serverData, mockRPC(route, { method, kwargs }, result) { if (method === "web_search_read") { assert.deepEqual(Object.keys(kwargs.specification), ["write_date", "id"]); return Promise.resolve({ length: 2, records: [ { id: 1, write_date: "2022-08-05 08:37:00" }, { id: 2, write_date: "2022-08-05 08:37:00" }, ], }); } }, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); assert.ok( target .querySelector(".o_activity_record img") .dataset.src.endsWith("/web/image?model=partner&field=image&id=2"), "image src is the preview image given in option" ); }); QUnit.test("test node is visible with invisible attribute on node", async function (assert) { const { target, openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[1, "activity"]], }); assert.containsN( target, ".invisible_node", 2, "The node with the invisible attribute should be displayed since the context does not have `invisible` key or has falsy value" ); }); QUnit.test( "test node is not displayed with invisible attribute on node", async function (assert) { const { target, openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[1, "activity"]], context: { invisible: true }, }); assert.containsNone( target, ".invisible_node", "The node with the invisible attribute should be displayed since `invisible` key in the context contains truly value" ); } ); QUnit.test("update activity view after creating multiple activities", async function (assert) { Object.assign(serverData.views, { "mail.test.activity,false,list": '', "mail.activity,false,form": '
', "mail.activity.schedule,false,form": "
", }); const activityToCreate = omit(pyEnv.mockServer.models["mail.activity"].records[0], "id"); pyEnv.mockServer.models["mail.activity"].records = []; pyEnv.mockServer.models["mail.activity.schedule"] = { fields: { id: { type: "integer" }, display_name: { type: "char" }, }, records: [], }; const { openView, target } = await start({ mockRPC(route, args) { if (args.method === "name_search") { args.kwargs.name = "MailTestActivity"; } if (args.method === "web_save" && args.model === "mail.activity.schedule") { pyEnv["mail.activity"].create(activityToCreate); } }, serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], }); assert.containsNone(target, ".o_activity_summary_cell"); await click(target, "table tfoot tr .o_record_selector"); await click( target, ".o_list_renderer table tbody tr:nth-child(2) td:nth-child(2) .o-mail-ActivityButton" ); await click(target, ".o-mail-ActivityListPopover > button.btn-secondary"); const modalSchedule = target.querySelector(".modal:has(.o_form_view)"); await insertText(`.o_form_view .o_field_widget[name='display_name'] input`, "test1", { target: modalSchedule, }); await click(modalSchedule, ".modal-footer button.o_form_button_save"); await click(target, ".modal-footer button.o_form_button_cancel"); assert.containsOnce(target, ".o_activity_summary_cell:not(.o_activity_empty_cell)"); }); QUnit.test( "Activity View: Hide 'New' button in SelectCreateDialog based on action context", async function (assert) { assert.expect(1); Object.assign(serverData.views, { "mail.test.activity,false,list": '', }); const { openView } = await start({ serverData, }); await openView({ res_model: "mail.test.activity", views: [[false, "activity"]], context: { create: false }, }); const activity = $(document); await testUtils.dom.click(activity.find("table tfoot tr .o_record_selector")); assert.containsNone(activity, ".o_create_button", "'New' button should be hidden."); } ); });