Odoo18-Base/addons/test_mail/static/tests/activity_tests.js
2025-03-10 10:52:11 +07:00

1525 lines
60 KiB
JavaScript

/* @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":
'<activity string="MailTestActivity">' +
"<templates>" +
'<div t-name="activity-box">' +
'<field name="name"/>' +
"</div>" +
"</templates>" +
"</activity>",
"mail.test.activity,1,activity": `
<activity string="MailTestActivity">
<div t-name="activity-box">
<span t-att-title="record.name.value">
<field name="name" display="full" class="w-100 text-truncate"/>
</span>
<span class="invisible_node" invisible="context.get('invisible', False)">
Test invisible
</span>
</div>
</activity>
`,
},
};
},
});
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":
'<tree string="MailTestActivity"><field name="name"/></tree>',
});
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":
'<tree string="MailTestActivity"><field name="name"/></tree>',
});
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":
'<tree string="MailTestActivity"><field name="name"/></tree>',
});
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": `<form>
<field name="display_name"/>
<footer>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</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": `<activity string="MailTestActivity">
<templates>
<div t-name="activity-box">
<field name="activity_user_id" widget="many2one_avatar_user"/>
<field name="name"/>
</div>
</templates>
</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": `<list><field name="name"/></list>`,
"mail.test.activity,false,search": `<search/>`,
"mail.test.activity,1,search": `<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": `
<activity string="MailTestActivity">
<templates>
<div t-name="activity-box">
<t t-if="luxon">
<span class="luxon">luxon</span>
</t>
</div>
</templates>
</activity>`,
});
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": `
<activity string="MailTestActivity">
<templates>
<div t-name="activity-box">
<img t-att-src="activity_image('partner', 'image', record.id.raw_value)"/>
<field name="id"/>
</div>
</templates>
</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":
'<tree string="MailTestActivity"><field name="name"/><field name="activity_ids" widget="list_activity"/></tree>',
"mail.activity,false,form": '<form><field name="activity_type_id"/></form>',
"mail.activity.schedule,false,form": "<form><field name='display_name'/></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":
'<tree string="MailTestActivity"><field name="name"/></tree>',
});
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.");
}
);
});