19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:39 +01:00
parent 38c6088dcc
commit d9452d2060
243 changed files with 30797 additions and 10815 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
import { describe, expect, test } from "@odoo/hoot";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { openView, start, startServer } from "@mail/../tests/mail_test_helpers";
describe.current.tags("mobile");
defineTestMailModels();
test("horizontal scroll applies only to the content, not to the whole controller", async () => {
const pyEnv = await startServer();
pyEnv["mail.activity.type"].create([
{ name: "Email" },
{ name: "Call" },
{ name: "Upload document" },
]);
await start();
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"]],
});
const o_view_controller = document.querySelector(".o_view_controller");
const o_content = o_view_controller.querySelector(".o_content");
const o_cp_item = document.querySelector(".o_breadcrumb .active");
const initialXCpItem = o_cp_item.getBoundingClientRect().x;
const o_header_cell = o_content.querySelector(".o_activity_type_cell");
const initialXHeaderCell = o_header_cell.getBoundingClientRect().x;
expect(o_view_controller).toHaveClass("o_action_delegate_scroll");
expect(o_view_controller).toHaveStyle({ overflow: "hidden" });
expect(o_content).toHaveStyle({ overflow: "auto" });
expect(o_content.scrollLeft).toBe(0);
o_content.scrollLeft = 100;
expect(o_content.scrollLeft).toBe(100);
expect(o_header_cell.getBoundingClientRect().x).toBeLessThan(initialXHeaderCell);
expect(o_cp_item).toHaveRect({ x: initialXCpItem });
});

View file

@ -1,676 +0,0 @@
/** @odoo-module **/
import ActivityRenderer from '@mail/js/views/activity/activity_renderer';
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import testUtils from 'web.test_utils';
import { click, insertText } from "@web/../tests/utils";
import { legacyExtraNextTick, patchWithCleanup} from "@web/../tests/helpers/utils";
import { doAction } from "@web/../tests/webclient/helpers";
import { session } from '@web/session';
let serverData;
let pyEnv;
QUnit.module('test_mail', {}, function () {
QUnit.module('activity view', {
async beforeEach() {
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: moment().add(3, "days").format("YYYY-MM-DD"), // now
can_write: true,
state: "planned",
activity_type_id: mailActivityTypeIds[0],
mail_template_ids: mailTemplateIds,
user_id: resUsersId1,
},
{
display_name: "An activity",
date_deadline: moment().format("YYYY-MM-DD"), // 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: moment().subtract(2, "days").format("YYYY-MM-DD"), // now
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,false,form':
'<form string="MailTestActivity">' +
'<field name="name"/>' +
'</form>',
},
};
}
});
var activityDateFormat = function (date) {
return date.toLocaleDateString(moment().locale(), { day: 'numeric', month: 'short' });
};
QUnit.test('activity view: simple activity rendering', async function (assert) {
assert.expect(15);
const mailTestActivityIds = pyEnv['mail.test.activity'].search([]);
const mailActivityTypeIds = pyEnv['mail.activity.type'].search([]);
const { click , env, openView } = await start({
serverData,
});
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"], [false, "form"]],
});
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');
var $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_legacy_kanban_counter', 'should contain a progressbar in header of first column');
assert.hasAttrValue($th1.find('.o_kanban_counter_progress .progress-bar:first'), 'data-bs-original-title', '1 Planned',
'the counter progressbars should be correctly displayed');
assert.hasAttrValue($th1.find('.o_kanban_counter_progress .progress-bar:nth-child(2)'), 'data-bs-original-title', '1 Today',
'the counter progressbars should be correctly displayed');
var $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_kanban_counter_progress .progress-bar:nth-child(3)'), 'data-bs-original-title', '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');
var today = activityDateFormat(new Date());
assert.ok($activity.find('table tbody tr:first td:nth-child(2).today .o_closest_deadline:contains(' + today + ')').length,
'should contain an activity for today in second cell of first line ' + today);
var 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');
// Ensure that the form view is opened in edit mode
await click(document.querySelector(".o_activity_record"));
const $form = $(document.querySelector('.o_form_view'));
assert.containsOnce($form, '.o_form_editable',
'Form view should be opened in edit mode');
});
QUnit.test('activity view: no content rendering', async function (assert) {
assert.expect(2);
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"]],
});
const $activity = $(document);
assert.containsOnce($activity, '.o_view_nocontent',
"should display the no content helper");
assert.strictEqual($activity.find('.o_view_nocontent .o_view_nocontent_empty_folder').text().trim(),
"No data to display",
"should display the no content helper text");
});
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 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_id: mailTestActivityId2,
default_template_id: mailTemplateId1,
default_use_template: true,
force_email: true
}, action.context);
assert.step("do_action_compose");
} else if (action.res_model === 'mail.activity') {
assert.deepEqual({
"default_activity_type_id": mailActivityTypeIds[1],
"default_res_id": mailTestActivityId2,
"default_res_model": 'mail.test.activity',
}, action.context);
assert.step("do_action_activity");
} else {
assert.step("Unexpected action");
}
return Promise.resolve();
},
});
await testUtils.dom.click(document.querySelector('.today .o_closest_deadline'));
assert.hasClass(document.querySelector('.today .dropdown-menu.o_activity'), 'show', "dropdown should be displayed");
assert.ok(document.querySelector('.o_activity_color_today').textContent.includes('Today'), "Title should be today");
assert.ok([...document.querySelectorAll('.today .o_activity_title_entry')].filter(el => el.textContent.includes('Template1')).length,
"Template1 should be available");
assert.ok([...document.querySelectorAll('.today .o_activity_title_entry')].filter(el => el.textContent.includes('Template2')).length,
"Template2 should be available");
await testUtils.dom.click(document.querySelector('.o_activity_title_entry[data-activity-id="2"] .o_activity_template_preview'));
await testUtils.dom.click(document.querySelector('.o_activity_title_entry[data-activity-id="2"] .o_activity_template_send'));
await testUtils.dom.click(document.querySelector('.overdue .o_closest_deadline'));
assert.notOk(document.querySelector('.overdue .o_activity_template_preview'),
"No template should be available");
await testUtils.dom.click(document.querySelector('.overdue .o_schedule_activity'));
await testUtils.dom.click(document.querySelector('.overdue .o_closest_deadline'));
await testUtils.dom.click(document.querySelector('.overdue .o_mark_as_done'));
document.querySelector('.overdue #activity_feedback').value = "feedback2";
await testUtils.dom.click(document.querySelector('.overdue .o_activity_popover_done_next'));
assert.verifySteps([
"do_action_compose",
"activity_send_mail",
"do_action_activity",
"action_feedback_schedule_next",
"serverGeneratedAction"
]);
});
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);
assert.containsN(
document.body,
".o_search_options .dropdown button:visible",
2,
"only two elements should be available in view search"
);
assert.isVisible(
document.querySelector(".o_search_options .dropdown.o_filter_menu > button"),
"filter should be available in view search"
);
assert.isVisible(
document.querySelector(".o_search_options .dropdown.o_favorite_menu > button"),
"favorites should be available in view search"
);
});
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');
var expectedAction = {
context: {
default_res_id: mailTestActivityId1,
default_res_model: "mail.test.activity",
},
name: "Schedule Activity",
res_id: false,
res_model: "mail.activity",
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
var $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: 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 legacyExtraNextTick();
assert.containsOnce($, ".modal.o_technical_modal", "Activity Modal should be opened");
await testUtils.dom.click($('.modal.o_technical_modal button[special="cancel"]'));
await legacyExtraNextTick();
assert.containsNone($, ".modal.o_technical_modal", "Activity Modal should be closed");
});
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 legacyExtraNextTick();
assert.containsN(document.body, '.o_m2o_avatar', 2);
assert.containsOnce(document.body, `tr[data-res-id=${mailTestActivityId1}] .o_m2o_avatar > img[data-src="/web/image/res.users/${resUsersId1}/avatar_128"]`,
"should have m2o avatar image");
assert.containsNone(document.body, '.o_m2o_avatar > span',
"should not have text on many2one_avatar_user if onlyImage node option is passed");
});
QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (assert) {
assert.expect(3);
patchWithCleanup(ActivityRenderer.prototype, {
setup() {
this._super();
owl.onMounted(() => {
assert.step('mounted');
});
owl.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 , click } = 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(9);
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);
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.containsOnce(document.querySelector('.o_activity_view tbody'), '.planned',
"other records should be available");
await testUtils.dom.click(document.querySelector('.o_kanban_counter_progress .progress-bar[data-filter="planned"]'));
assert.containsOnce(document.querySelector('.o_activity_view thead'), '.o_activity_filter_planned',
"planned should be active filter");
assert.containsN(document.querySelector('.o_activity_view tbody'), '.o_activity_filter_planned', 5,
"planned should be active filter");
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: 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"]],
});
assert.containsN(document.body, ".luxon", 2);
});
QUnit.test('update activity view after creating multiple activities', async function (assert) {
assert.expect(9);
pyEnv['mail.test.activity'].create({ name: 'MailTestActivity 3' });
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>'
});
const { 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']],
});
await click("table tfoot tr .o_record_selector");
await click(".o_list_renderer table tbody tr:nth-child(2) td:nth-child(2) .o_ActivityButtonView")
await click(".o-main-components-container .o_PopoverManager .o_ActivityListView .o_ActivityListView_addActivityButton");
await insertText('.o_field_many2one_selection .o_input_dropdown .dropdown input[id=activity_type_id]', "test1");
await click(".o_field_many2one_selection .o_input_dropdown .dropdown input[id=activity_type_id]");
await click('.o-autocomplete--dropdown-menu li:nth-child(1) .dropdown-item');
await click(".modal-footer .o_cp_buttons .o_form_buttons_edit .btn-primary");
await click(".modal-footer .o_form_button_cancel");
await click("table tbody tr:nth-child(1) td:nth-child(6) .o_mail_activity .o_activity_btn .o_closest_deadline");
});
});

View file

@ -0,0 +1,252 @@
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { beforeEach, describe, test, expect } from "@odoo/hoot";
import { queryOne, waitUntil } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import {
click,
contains,
openFormView,
registerArchs,
start,
startServer,
patchUiSize,
SIZES,
dragenterFiles,
dropFiles,
} from "@mail/../tests/mail_test_helpers";
import { browser } from "@web/core/browser/browser";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineTestMailModels();
let popoutIframe, popoutWindow;
beforeEach(() => {
popoutIframe = document.createElement("iframe");
popoutWindow = {
closed: false,
get document() {
const doc = popoutIframe.contentDocument;
if (!doc) {
return undefined;
}
const originalWrite = doc.write;
doc.write = (content) => {
// This avoids duplicating the test script in the popoutWindow
const sanitizedContent = content.replace(
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
""
);
originalWrite.call(doc, sanitizedContent);
};
return doc;
},
close: () => {
popoutWindow.closed = true;
popoutIframe.remove(popoutAttachmentViewBody());
},
};
});
patchWithCleanup(browser, {
open: () => {
popoutWindow.closed = false;
queryOne(".o_popout_holder").append(popoutIframe);
return popoutWindow;
},
});
function popoutAttachmentViewBody() {
return popoutWindow.document.querySelector(".o-mail-PopoutAttachmentView");
}
async function popoutIsEmpty() {
await animationFrame();
expect(popoutAttachmentViewBody()).toBe(null);
}
async function popoutContains(selector) {
await animationFrame();
await waitUntil(() => popoutAttachmentViewBody());
const target = popoutAttachmentViewBody().querySelector(selector);
expect(target).toBeDisplayed();
return target;
}
async function popoutClick(selector) {
const target = await popoutContains(selector);
click(target);
}
test("Attachment view popout controls test", async () => {
/*
* This test makes sure that the attachment view controls are working in the following cases:
* - Inside the popout window
* - After closing the popout window
*/
const pyEnv = await startServer();
const recordId = pyEnv["mail.test.simple.main.attachment"].create({
display_name: "first partner",
message_attachment_count: 2,
});
pyEnv["ir.attachment"].create([
{
mimetype: "image/jpeg",
res_id: recordId,
res_model: "mail.test.simple.main.attachment",
},
{
mimetype: "application/pdf",
res_id: recordId,
res_model: "mail.test.simple.main.attachment",
},
]);
registerArchs({
"mail.test.simple.main.attachment,false,form": `
<form string="Test document">
<div class="o_popout_holder"/>
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview"/>
<chatter/>
</form>`,
});
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordId);
await click(".o_attachment_preview .o_attachment_control");
await animationFrame();
expect(".o_attachment_preview").not.toBeVisible();
await popoutClick(".o_move_next");
await popoutContains("img");
await popoutClick(".o_move_previous");
await popoutContains("iframe");
popoutWindow.close();
await contains(".o_attachment_preview:not(.d-none)");
expect(".o_attachment_preview").toBeVisible();
await click(".o_attachment_preview .o_move_next");
await contains(".o_attachment_preview img");
await click(".o_attachment_preview .o_move_previous");
await contains(".o_attachment_preview iframe");
await click(".o_attachment_preview .o_attachment_control");
await animationFrame();
expect(".o_attachment_preview").not.toBeVisible();
});
test("Chatter main attachment: can change from non-viewable to viewable", async () => {
const pyEnv = await startServer();
const recordId = pyEnv['mail.test.simple.main.attachment'].create({});
const irAttachmentId = pyEnv['ir.attachment'].create({
mimetype: 'text/plain',
name: "Blah.txt",
res_id: recordId,
res_model: 'mail.test.simple.main.attachment',
});
pyEnv['mail.message'].create({
attachment_ids: [irAttachmentId],
model: 'mail.test.simple.main.attachment',
res_id: recordId,
});
pyEnv['mail.test.simple.main.attachment'].write([recordId], {message_main_attachment_id : irAttachmentId});
registerArchs({
"mail.test.simple.main.attachment,false,form": `
<form string="Test document">
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview"/>
<chatter/>
</form>`,
});
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordId);
// Add a PDF file
const pdfFile = new File([new Uint8Array(1)], "text.pdf", { type: "application/pdf" });
await dragenterFiles(".o-mail-Chatter", [pdfFile]);
await dropFiles(".o-Dropzone", [pdfFile]);
await contains(".o_attachment_preview");
await contains(".o-mail-Attachment > iframe", { count: 0 }); // The viewer tries to display the text file not the PDF
// Switch to the PDF file in the viewer
await click(".o_move_next");
await contains(".o-mail-Attachment > iframe"); // There should be iframe for PDF viewer
});
test.skip("Attachment view / chatter popout across multiple records test", async () => {
// skip because test has race conditions: https://runbot.odoo.com/odoo/runbot.build.error/109795
const pyEnv = await startServer();
const recordIds = pyEnv["mail.test.simple.main.attachment"].create([
{
display_name: "first partner",
message_attachment_count: 1,
},
{
display_name: "second partner",
message_attachment_count: 0,
},
{
display_name: "third partner",
message_attachment_count: 1,
},
]);
pyEnv["ir.attachment"].create([
{
mimetype: "image/jpeg",
res_id: recordIds[0],
res_model: "mail.test.simple.main.attachment",
},
{
mimetype: "application/pdf",
res_id: recordIds[2],
res_model: "mail.test.simple.main.attachment",
},
]);
registerArchs({
"mail.test.simple.main.attachment,false,form": `
<form string="Test document">
<div class="o_popout_holder"/>
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview"/>
<chatter/>
</form>`,
});
async function navigateRecords() {
/**
* It should be called on the first record of recordIds
* The popout window should be open
* It navigates recordIds as 0 -> 1 -> 2 -> 0 -> 2
*/
await animationFrame();
expect(".o_attachment_preview").not.toBeVisible();
await popoutContains("img");
await click(".o_pager_next");
await popoutIsEmpty();
await click(".o_pager_next");
await popoutContains("iframe");
await click(".o_pager_next");
await popoutContains("img");
await click(".o_pager_previous");
await popoutContains("iframe");
popoutWindow.close();
await contains(".o_attachment_preview:not(.d-none)");
}
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordIds[0], {
resIds: recordIds,
});
await click(".o_attachment_preview .o_attachment_control");
await navigateRecords();
await openFormView("mail.test.simple.main.attachment", recordIds[0], {
resIds: recordIds,
});
await click("button i[title='Pop out Attachments']");
await navigateRecords();
});

View file

@ -0,0 +1,197 @@
import {
click,
contains,
inputFiles,
insertText,
listenStoreFetch,
openFormView,
patchUiSize,
registerArchs,
SIZES,
start,
startServer,
triggerHotkey,
waitStoreFetch,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { MockServer, onRpc } from "@web/../tests/web_test_helpers";
import { mail_data } from "@mail/../tests/mock_server/mail_mock_server";
describe.current.tags("desktop");
defineTestMailModels();
test("Send message button activation (access rights dependent)", async () => {
const pyEnv = await startServer();
registerArchs({
"mail.test.multi.company,false,form": `
<form string="Simple">
<sheet>
<field name="name"/>
</sheet>
<chatter/>
</form>
`,
"mail.test.multi.company.read,false,form": `
<form string="Simple">
<sheet>
<field name="name"/>
</sheet>
<chatter/>
</form>
`,
});
let userAccess = {};
listenStoreFetch("mail.thread", {
async onRpc(request) {
const { params } = await request.json();
if (params.fetch_params.some((fetchParam) => fetchParam[0] === "mail.thread")) {
const res = await mail_data.bind(MockServer.current)(request);
res["mail.thread"][0].hasWriteAccess = userAccess.hasWriteAccess;
res["mail.thread"][0].hasReadAccess = userAccess.hasReadAccess;
return res;
}
},
});
await start();
const simpleId = pyEnv["mail.test.multi.company"].create({ name: "Test MC Simple" });
const simpleMcId = pyEnv["mail.test.multi.company.read"].create({
name: "Test MC Readonly with Activities",
});
async function assertSendButton(
enabled,
activities,
msg,
model = null,
resId = null,
hasReadAccess = false,
hasWriteAccess = false
) {
userAccess = { hasReadAccess, hasWriteAccess };
await openFormView(model, resId);
if (resId) {
await waitStoreFetch("mail.thread");
}
if (enabled) {
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Send message" });
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Log note" });
if (activities) {
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Activity" });
}
} else {
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Send message" });
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Log note" });
if (activities) {
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Activity" });
}
}
}
await assertSendButton(
true,
false,
"Record, all rights",
"mail.test.multi.company",
simpleId,
true,
true
);
await assertSendButton(
true,
true,
"Record, all rights",
"mail.test.multi.company.read",
simpleId,
true,
true
);
await assertSendButton(
false,
false,
"Record, no write access",
"mail.test.multi.company",
simpleId,
true
);
await assertSendButton(
true,
true,
"Record, read access but model accept post with read only access",
"mail.test.multi.company.read",
simpleMcId,
true
);
await assertSendButton(false, false, "Record, no rights", "mail.test.multi.company", simpleId);
await assertSendButton(false, true, "Record, no rights", "mail.test.multi.company.read", simpleMcId);
// Note that rights have no impact on send button for draft record (chatter.isTemporary=true)
await assertSendButton(true, false, "Draft record", "mail.test.multi.company");
await assertSendButton(true, true, "Draft record", "mail.test.multi.company.read");
});
test("basic chatter rendering with a model without activities", async () => {
const pyEnv = await startServer();
const recordId = pyEnv["mail.test.simple"].create({ name: "new record" });
registerArchs({
"mail.test.simple,false,form": `
<form string="Records">
<sheet>
<field name="name"/>
</sheet>
<chatter/>
</form>
`,
});
await start();
await openFormView("mail.test.simple", recordId);
await contains(".o-mail-Chatter");
await contains(".o-mail-Chatter-topbar");
await contains("button[aria-label='Attach files']");
await contains("button", { count: 0, text: "Activities" });
await contains(".o-mail-Followers");
await contains(".o-mail-Thread");
});
test("opened attachment box should remain open after adding a new attachment", async (assert) => {
const pyEnv = await startServer();
const recordId = pyEnv["mail.test.simple.main.attachment"].create({});
const attachmentId = pyEnv["ir.attachment"].create({
mimetype: "image/jpeg",
res_id: recordId,
res_model: "mail.test.simple.main.attachment",
});
pyEnv["mail.message"].create({
attachment_ids: [attachmentId],
model: "mail.test.simple.main.attachment",
res_id: recordId,
});
onRpc("/mail/thread/data", async (request) => {
await new Promise((resolve) => setTimeout(resolve, 1)); // need extra time for useEffect
});
patchUiSize({ size: SIZES.XXL });
await start();
await openFormView("mail.test.simple.main.attachment", recordId, {
arch: `
<form>
<sheet>
<field name="name"/>
</sheet>
<div class="o_attachment_preview" />
<chatter reload_on_post="True" reload_on_attachment="True"/>
</form>`,
});
await contains(".o_attachment_preview");
await click(".o-mail-Chatter-attachFiles");
await contains(".o-mail-AttachmentBox");
await click("button", { text: "Send message" });
await inputFiles(".o-mail-Composer .o_input_file", [
new File(["image"], "testing.jpeg", { type: "image/jpeg" }),
]);
await click(".o-mail-Composer-send:enabled");
await contains(".o_move_next");
await click("button", { text: "Send message" });
await insertText(".o-mail-Composer-input", "test");
triggerHotkey("control+Enter");
await contains(".o-mail-Message-body", { text: "test" });
await contains(".o-mail-AttachmentBox .o-mail-AttachmentImage", { count: 2 });
});

View file

@ -1,77 +0,0 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('mail', {}, function () {
QUnit.module('Chatter');
QUnit.test('Send message button activation (access rights dependent)', async function (assert) {
const pyEnv = await startServer();
const view = `<form string="Simple">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>`;
let userAccess = {};
const { openView } = await start({
serverData: {
views: {
'mail.test.multi.company,false,form': view,
'mail.test.multi.company.read,false,form': view,
}
},
async mockRPC(route, args, performRPC) {
const res = await performRPC(route, args);
if (route === '/mail/thread/data') {
// mimic user with custom access defined in userAccess variable
const { thread_model } = args;
Object.assign(res, userAccess);
res['canPostOnReadonly'] = thread_model === 'mail.test.multi.company.read';
}
return res;
},
});
const resSimpleId1 = pyEnv['mail.test.multi.company'].create({ name: 'Test MC Simple' });
const resSimpleMCId1 = pyEnv['mail.test.multi.company.read'].create({ name: 'Test MC Readonly' });
async function assertSendButton(enabled, msg,
model = null, resId = null,
hasReadAccess = false, hasWriteAccess = false) {
userAccess = { hasReadAccess, hasWriteAccess };
await openView({
res_id: resId,
res_model: model,
views: [[false, 'form']],
});
const details = `hasReadAccess: ${hasReadAccess}, hasWriteAccess: ${hasWriteAccess}, model: ${model}, resId: ${resId}`;
if (enabled) {
assert.containsNone(document.body, '.o_ChatterTopbar_buttonSendMessage:disabled',
`${msg}: send message button must not be disabled (${details}`);
} else {
assert.containsOnce(document.body, '.o_ChatterTopbar_buttonSendMessage:disabled',
`${msg}: send message button must be disabled (${details})`);
}
}
const enabled = true, disabled = false;
await assertSendButton(enabled, 'Record, all rights', 'mail.test.multi.company', resSimpleId1, true, true);
await assertSendButton(enabled, 'Record, all rights', 'mail.test.multi.company.read', resSimpleId1, true, true);
await assertSendButton(disabled, 'Record, no write access', 'mail.test.multi.company', resSimpleId1, true);
await assertSendButton(enabled, 'Record, read access but model accept post with read only access',
'mail.test.multi.company.read', resSimpleMCId1, true);
await assertSendButton(disabled, 'Record, no rights', 'mail.test.multi.company', resSimpleId1);
await assertSendButton(disabled, 'Record, no rights', 'mail.test.multi.company.read', resSimpleMCId1);
// Note that rights have no impact on send button for draft record (chatter.isTemporary=true)
await assertSendButton(enabled, 'Draft record', 'mail.test.multi.company');
await assertSendButton(enabled, 'Draft record', 'mail.test.multi.company.read');
});
});

View file

@ -1,5 +0,0 @@
/** @odoo-module **/
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch(['mail.test.track.all', 'mail.test.activity', 'mail.test.multi.company', 'mail.test.multi.company.read']);

View file

@ -1,48 +0,0 @@
/** @odoo-module **/
import { start } from "@mail/../tests/helpers/test_utils";
import { prepareTarget } from "web.test_utils";
QUnit.module("test_mail", () => {
QUnit.module("activity view mobile");
QUnit.test('horizontal scroll applies only to the content, not to the whole controller', async (assert) => {
const viewPort = prepareTarget();
viewPort.style.position = "initial";
viewPort.style.width = "initial";
const { openView } = await start();
await openView({
res_model: "mail.test.activity",
views: [[false, "activity"]],
});
const o_view_controller = document.querySelector(".o_view_controller");
const o_content = o_view_controller.querySelector(".o_content");
const o_cp_buttons = o_view_controller.querySelector(".o_control_panel .o_cp_buttons");
const initialXCpBtn = o_cp_buttons.getBoundingClientRect().x;
const o_header_cell = o_content.querySelector(".o_activity_type_cell");
const initialXHeaderCell = o_header_cell.getBoundingClientRect().x;
assert.hasClass(o_view_controller, "o_action_delegate_scroll",
"the 'o_view_controller' should be have the 'o_action_delegate_scroll'.");
assert.strictEqual(window.getComputedStyle(o_view_controller).overflow,"hidden",
"the view controller should have overflow hidden");
assert.strictEqual(window.getComputedStyle(o_content).overflow,"auto",
"the view content should have the overflow auto");
assert.strictEqual(o_content.scrollLeft, 0, "the o_content should not have scroll value");
// Horizontal scroll
o_content.scrollLeft = 100;
assert.strictEqual(o_content.scrollLeft, 100, "the o_content should be 100 due to the overflow auto");
assert.ok(o_header_cell.getBoundingClientRect().x < initialXHeaderCell,
"the gantt header cell x position value should be lower after the scroll");
assert.strictEqual(o_cp_buttons.getBoundingClientRect().x, initialXCpBtn,
"the btn x position of the control panel button should be the same after the scroll");
viewPort.style.position = "";
viewPort.style.width = "";
});
});

View file

@ -0,0 +1,6 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestActivity extends models.ServerModel {
_name = "mail.test.activity";
_inherit = ["mail.thread"];
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestMultiCompany extends models.ServerModel {
_name = "mail.test.multi.company";
}

View file

@ -0,0 +1,6 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestMultiCompanyRead extends models.ServerModel {
_name = "mail.test.multi.company.read";
_mail_post_access = "read";
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestProperties extends models.ServerModel {
_name = "mail.test.properties";
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestSimple extends models.ServerModel {
_name = "mail.test.simple";
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class MailTestSimpleMainAttachment extends models.ServerModel {
_name = "mail.test.simple.main.attachment";
}

View file

@ -0,0 +1,10 @@
import { fields, models } from "@web/../tests/web_test_helpers";
export class MailTestTrackAll extends models.ServerModel {
_name = "mail.test.track.all";
_inherit = ["mail.thread"];
float_field_with_digits = fields.Float({
digits: [10, 8],
});
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class ResCurrency extends models.ServerModel {
_name = "res.currency";
}

View file

@ -0,0 +1,73 @@
import {
click,
contains,
openFormView,
registerArchs,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { asyncStep, onRpc, waitForSteps } from "@web/../tests/web_test_helpers";
/**
* Open a chat window when clicking on an avatar many2one / many2many properties.
*/
async function testPropertyFieldAvatarOpenChat(propertyType) {
const pyEnv = await startServer();
registerArchs({
"mail.test.properties,false,form": `
<form string="Form With Avatar Users">
<sheet>
<field name="name"/>
<field name="parent_id"/>
<field name="properties"/>
</sheet>
<chatter/>
</form>
`,
});
onRpc("mail.test.properties", "has_access", () => true);
onRpc("res.users", "read", () => {
asyncStep("read res.users");
return [{ id: userId, partner_id: [partnerId, "Partner Test"] }];
});
onRpc("res.users", "search_read", () => [{ id: userId, name: "User Test" }]);
await start();
const partnerId = pyEnv["res.partner"].create({ name: "Partner Test" });
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
const propertyDefinition = {
type: propertyType,
comodel: "res.users",
name: "user",
string: "user",
};
const parentId = pyEnv["mail.test.properties"].create({
name: "Parent",
definition_properties: [propertyDefinition],
});
const childId = pyEnv["mail.test.properties"].create({
name: "Test",
parent_id: parentId,
properties: [{ ...propertyDefinition, value: [userId] }],
});
await openFormView("mail.test.properties", childId);
await waitForSteps([]);
await click(
propertyType === "many2one" ? ".o_field_property_many2one_value img" : ".o_m2m_avatar"
);
await waitForSteps(["read res.users"]);
await contains(".o-mail-ChatWindow", { text: "Partner Test" });
}
describe.current.tags("desktop");
defineTestMailModels();
test("Properties fields: many2one avatar open chat on click", async () => {
await testPropertyFieldAvatarOpenChat("many2one");
});
test("Properties fields: m2m avatar list open chat on click", async () => {
await testPropertyFieldAvatarOpenChat("many2many");
});

View file

@ -0,0 +1,129 @@
import { start, startServer } from "@mail/../tests/mail_test_helpers";
import { click, contains } from "@mail/../tests/mail_test_helpers_contains";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { asyncStep, mockService, waitForSteps } from "@web/../tests/web_test_helpers";
import { serializeDate, today } from "@web/core/l10n/dates";
describe.current.tags("desktop");
defineTestMailModels();
// Avoid problem around midnight (Ex.: tomorrow activities become today activities when reaching midnight)
beforeEach(() => mockDate("2023-04-08 10:00:00", 0));
test("menu with no records", async () => {
await start();
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Activities'])");
await contains(".o-mail-ActivityMenu", {
text: "Congratulations, you're done with your activities.",
});
});
test("do not show empty text when at least some future activities", async () => {
const tomorrow = today().plus({ days: 1 });
const pyEnv = await startServer();
const activityId = pyEnv["mail.test.activity"].create({});
pyEnv["mail.activity"].create([
{
date_deadline: serializeDate(tomorrow),
res_id: activityId,
res_model: "mail.test.activity",
},
]);
await start();
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Activities'])");
await contains(".o-mail-ActivityMenu", {
count: 0,
text: "Congratulations, you're done with your activities.",
});
});
test("activity menu widget: activity menu with 2 models", async () => {
const tomorrow = today().plus({ days: 1 });
const yesterday = today().plus({ days: -1 });
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
const activityIds = pyEnv["mail.test.activity"].create([{}, {}, {}, {}]);
pyEnv["mail.activity"].create([
{ res_id: partnerId, res_model: "res.partner", date_deadline: serializeDate(today()) },
{
res_id: activityIds[0],
res_model: "mail.test.activity",
date_deadline: serializeDate(today()),
},
{
date_deadline: serializeDate(tomorrow),
res_id: activityIds[1],
res_model: "mail.test.activity",
},
{
date_deadline: serializeDate(tomorrow),
res_id: activityIds[2],
res_model: "mail.test.activity",
},
{
date_deadline: serializeDate(yesterday),
res_id: activityIds[3],
res_model: "mail.test.activity",
},
]);
await start();
await contains(".o_menu_systray i[aria-label='Activities']");
await contains(".o-mail-ActivityMenu-counter");
await contains(".o-mail-ActivityMenu-counter", { text: "5" });
const actionChecks = {
context: {
force_search_count: 1,
search_default_filter_activities_my: 1,
search_default_activities_overdue: 1,
search_default_activities_today: 1,
},
domain: [["active", "in", [true, false]]],
};
mockService("action", {
doAction(action) {
Object.entries(actionChecks).forEach(([key, value]) => {
if (Array.isArray(value) || typeof value === "object") {
expect(action[key]).toEqual(value);
} else {
expect(action[key]).toBe(value);
}
});
asyncStep("do_action:" + action.name);
},
});
await click(".o_menu_systray i[aria-label='Activities']");
await contains(".o-mail-ActivityMenu");
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", { count: 2 });
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", {
contains: [
["div[name='activityTitle']", { text: "res.partner" }],
["span", { text: "0 Late" }],
["span", { text: "1 Today" }],
["span", { text: "0 Future" }],
],
});
await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", {
contains: [
["div[name='activityTitle']", { text: "mail.test.activity" }],
["span", { text: "1 Late" }],
["span", { text: "1 Today" }],
["span", { text: "2 Future" }],
],
});
actionChecks.res_model = "res.partner";
await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "res.partner" });
await contains(".o-mail-ActivityMenu", { count: 0 });
await click(".o_menu_systray i[aria-label='Activities']");
actionChecks.res_model = "mail.test.activity";
await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "mail.test.activity" });
await waitForSteps(["do_action:res.partner", "do_action:mail.test.activity"]);
});
test("activity menu widget: close on messaging menu click", async () => {
await start();
await click(".o_menu_systray i[aria-label='Activities']");
await contains(".o-mail-ActivityMenu");
await click(".o_menu_systray i[aria-label='Messages']");
await contains(".o-mail-ActivityMenu", { count: 0 });
});

View file

@ -1,165 +0,0 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import session from 'web.session';
import { date_to_str } from 'web.time';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('test_mail', {}, function () {
QUnit.module('systray_activity_menu_tests.js', {
async beforeEach() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const mailTestActivityIds = pyEnv['mail.test.activity'].create([{}, {}, {}, {}]);
pyEnv['mail.activity'].create([
{ res_id: resPartnerId1, res_model: 'res.partner' },
{ res_id: mailTestActivityIds[0], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[1], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[2], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(yesterday), res_id: mailTestActivityIds[3], res_model: 'mail.test.activity' },
]);
},
});
QUnit.test('activity menu widget: menu with no records', async function (assert) {
assert.expect(1);
const { click } = await start({
mockRPC: function (route, args) {
if (args.method === 'systray_get_activities') {
return Promise.resolve([]);
}
},
});
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsOnce(document.body, '.o_ActivityMenuView_noActivity');
});
QUnit.test('activity menu widget: activity menu with 2 models', async function (assert) {
assert.expect(10);
const { click, env } = await start();
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsOnce(document.body, '.o_ActivityMenuView', 'should contain an instance of widget');
assert.ok(document.querySelectorAll('.o_ActivityMenuView_activityGroup').length);
assert.containsOnce(document.body, '.o_ActivityMenuView_counter', "widget should have notification counter");
assert.strictEqual(parseInt(document.querySelector('.o_ActivityMenuView_counter').innerText), 5, "widget should have 5 notification counter");
var context = {};
patchWithCleanup(env.services.action, {
doAction(action) {
assert.deepEqual(action.context, context, "wrong context value");
},
});
// case 1: click on "late"
context = {
force_search_count: 1,
search_default_activities_overdue: 1,
};
assert.containsOnce(document.body, '.o_ActivityMenuView_dropdownMenu.show', 'ActivityMenu should be open');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="overdue"]');
assert.containsNone(document.body, '.show', 'ActivityMenu should be closed');
// case 2: click on "today"
context = {
force_search_count: 1,
search_default_activities_today: 1,
};
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="today"]');
// case 3: click on "future"
context = {
force_search_count: 1,
search_default_activities_upcoming_all: 1,
};
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="upcoming_all"]');
// case 4: click anywere else
context = {
force_search_count: 1,
search_default_activities_overdue: 1,
search_default_activities_today: 1,
};
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroups > div[data-model_name="mail.test.activity"]');
});
QUnit.test('activity menu widget: activity view icon', async function (assert) {
assert.expect(14);
patchWithCleanup(session, { uid: 10 });
const { click, env } = await start();
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsN(document.body, '.o_ActivityMenuView_activityGroupActionButton', 2,
"widget should have 2 activity view icons");
var first = document.querySelector('.o_ActivityMenuView_activityGroupActionButton[data-model_name="res.partner"]');
var second = document.querySelector('.o_ActivityMenuView_activityGroupActionButton[data-model_name="mail.test.activity"]');
assert.ok(first, "should have activity action linked to 'res.partner'");
assert.hasClass(first, 'fa-clock-o', "should display the activity action icon");
assert.ok(second, "should have activity action linked to 'mail.test.activity'");
assert.hasClass(second, 'fa-clock-o', "should display the activity action icon");
patchWithCleanup(env.services.action, {
doAction(action) {
if (action.name) {
assert.ok(action.domain, "should define a domain on the action");
assert.deepEqual(action.domain, [["activity_ids.user_id", "=", 10]],
"should set domain to user's activity only");
assert.step('do_action:' + action.name);
} else {
assert.step('do_action:' + action);
}
},
});
assert.hasClass(document.querySelector('.o-dropdown-menu'), 'show',
"dropdown should be expanded");
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="mail.test.activity"]');
assert.containsNone(document.body, '.o-dropdown-menu',
"dropdown should be collapsed");
// click on the "res.partner" activity icon
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="res.partner"]');
assert.verifySteps([
'do_action:mail.test.activity',
'do_action:res.partner'
]);
});
QUnit.test('activity menu widget: close on messaging menu click', async function (assert) {
assert.expect(2);
const { click } = await start();
await click('.dropdown-toggle[title="Activities"]');
assert.hasClass(
document.querySelector('.o_ActivityMenuView_dropdownMenu'),
'show',
"activity menu should be shown after click on itself"
);
await click(`.o_MessagingMenu_toggler`);
assert.containsNone(
document.body,
'.o_ActivityMenuView_dropdownMenu',
"activity menu should be hidden after click on messaging menu"
);
});
});

View file

@ -0,0 +1,32 @@
import { contains, mailModels } from "@mail/../tests/mail_test_helpers";
import { MailTestActivity } from "@test_mail/../tests/mock_server/models/mail_test_activity";
import { MailTestMultiCompany } from "@test_mail/../tests/mock_server/models/mail_test_multi_company";
import { MailTestMultiCompanyRead } from "@test_mail/../tests/mock_server/models/mail_test_multi_company_read";
import { MailTestProperties } from "@test_mail/../tests/mock_server/models/mail_test_properties";
import { MailTestSimpleMainAttachment } from "./mock_server/models/mail_test_simple_main_attachment";
import { MailTestSimple } from "@test_mail/../tests/mock_server/models/mail_test_simple";
import { MailTestTrackAll } from "@test_mail/../tests/mock_server/models/mail_test_track_all";
import { defineModels, defineParams } from "@web/../tests/web_test_helpers";
export const testMailModels = {
...mailModels,
MailTestActivity,
MailTestMultiCompany,
MailTestMultiCompanyRead,
MailTestProperties,
MailTestSimpleMainAttachment,
MailTestSimple,
MailTestTrackAll,
};
export function defineTestMailModels() {
defineParams({ suite: "test_mail" }, "replace");
defineModels(testMailModels);
}
export async function editSelect(selector, value) {
await contains(selector);
const el = document.querySelector(selector);
el.value = value;
el.dispatchEvent(new Event("change"));
}

View file

@ -0,0 +1,69 @@
import { registry } from "@web/core/registry";
const setPager = value => [
{
content: "Click Pager",
trigger: ".o_pager_value:first()",
run: "click",
},
{
content: "Change pager to display lines " + value,
trigger: "input.o_pager_value",
run: `edit ${value} && click body`,
},
{
trigger: `.o_pager_value:contains('${value}')`,
},
]
const checkRows = values => {
return {
trigger: '.o_activity_view',
run: () => {
const dataRow = document.querySelectorAll('.o_activity_view tbody .o_data_row .o_activity_record');
if (dataRow.length !== values.length) {
throw Error(`There should be ${values.length} activities`);
}
values.forEach((value, index) => {
if (dataRow[index].textContent !== value) {
throw Error(`Record does not match ${value} != ${dataRow[index]}`);
}
});
}
}
}
registry.category("web_tour.tours").add("mail_activity_view", {
steps: () => [
{
content: "Open the debug menu",
trigger: ".o_debug_manager button",
run: "click",
},
{
content: "Click the Set Defaults menu",
trigger: ".o-dropdown-item:contains(Open View)",
run: "click",
},
{
trigger: ".o_searchview_input",
run: "edit Test Activity View"
},
{
trigger: ".o_searchview_autocomplete .o-dropdown-item.focus",
content: "Validate search",
run: "click",
},
{
content: "Select Test Activity View",
trigger: `.o_data_row td:contains("Test Activity View")`,
run: "click",
},
checkRows(["Task 1", "Task 2", "Task 3"]),
...setPager("1-2"),
checkRows(["Task 2", "Task 3"]),
...setPager("3"),
checkRows(["Task 1"]),
],
})

View file

@ -0,0 +1,377 @@
import {
contains,
click,
insertText,
openFormView,
registerArchs,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { mockDate, mockTimeZone } from "@odoo/hoot-mock";
import { defineTestMailModels } from "@test_mail/../tests/test_mail_test_helpers";
import { editSelectMenu, patchWithCleanup } from "@web/../tests/web_test_helpers";
import { currencies } from "@web/core/currency";
const archs = {
"mail.test.track.all,false,form": `
<form>
<sheet>
<field name="boolean_field"/>
<field name="char_field"/>
<field name="date_field"/>
<field name="datetime_field"/>
<field name="float_field"/>
<field name="float_field_with_digits"/>
<field name="integer_field"/>
<field name="monetary_field"/>
<field name="many2one_field_id"/>
<field name="selection_field"/>
<field name="text_field"/>
</sheet>
<chatter/>
</form>
`,
};
describe.current.tags("desktop");
defineTestMailModels();
beforeEach(() => mockTimeZone(0));
test("basic rendering of tracking value (float type)", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({ float_field: 12.3 });
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=float_field] input", "45.67", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking");
await contains(".o-mail-Message-trackingField");
await contains(".o-mail-Message-trackingField", { text: "(Float)" });
await contains(".o-mail-Message-trackingOld");
await contains(".o-mail-Message-trackingOld", { text: "12.30" });
await contains(".o-mail-Message-trackingSeparator");
await contains(".o-mail-Message-trackingNew");
await contains(".o-mail-Message-trackingNew", { text: "45.67" });
});
test("rendering of tracked field of type float: from non-0 to 0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
float_field: 1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=float_field] input", "0", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "1.000.00(Float)" });
});
test("rendering of tracked field of type float: from 0 to non-0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
float_field: 0,
float_field_with_digits: 0,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=float_field] input", "1.01", { replace: true });
await insertText("div[name=float_field_with_digits] input", "1.0001", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { count: 2 });
const [increasedPrecisionLine, defaultPrecisionLine] =
document.getElementsByClassName("o-mail-Message-tracking");
const expectedText = [
[defaultPrecisionLine, ["0.00", "1.01", "(Float)"]],
[increasedPrecisionLine, ["0.00000000", "1.00010000", "(Float)"]],
];
for (const [targetLine, [oldText, newText, fieldName]] of expectedText) {
await contains(".o-mail-Message-trackingOld", { target: targetLine, text: oldText });
await contains(".o-mail-Message-trackingNew", { target: targetLine, text: newText });
await contains(".o-mail-Message-trackingField", { target: targetLine, text: fieldName });
}
});
test("rendering of tracked field of type integer: from non-0 to 0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
integer_field: 1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=integer_field] input", "0", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "10(Integer)" });
});
test("rendering of tracked field of type integer: from 0 to non-0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
integer_field: 0,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=integer_field] input", "1", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "01(Integer)" });
});
test("rendering of tracked field of type monetary: from non-0 to 0", async () => {
const pyEnv = await startServer();
const testCurrencyId = pyEnv["res.currency"].create({ name: "ECU", symbol: "§" });
// need to patch currencies as they're passed via cookies, not through the orm
patchWithCleanup(currencies, {
[testCurrencyId]: { digits: [69, 2], position: "after", symbol: "§" },
});
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
currency_id: testCurrencyId,
monetary_field: 1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=monetary_field] input", "0", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "1.00 §0.00 §(Monetary)" });
});
test("rendering of tracked field of type monetary: from 0 to non-0", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
monetary_field: 0,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=monetary_field] input", "1", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "0.001.00(Monetary)" });
});
test("rendering of tracked field of type boolean: from true to false", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
boolean_field: true,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click(".o_field_boolean input");
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "YesNo(Boolean)" });
});
test("rendering of tracked field of type boolean: from false to true", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click(".o_field_boolean input");
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoYes(Boolean)" });
});
test("rendering of tracked field of type char: from a string to empty string", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
char_field: "Marc",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=char_field] input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "MarcNone(Char)" });
});
test("rendering of tracked field of type char: from empty string to a string", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
char_field: "",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=char_field] input", "Marc", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Char)" });
});
test("rendering of tracked field of type date: from no date to a set date", async () => {
mockDate("2018-12-01");
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
date_field: false,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=date_field] input");
await click(".o_datetime_button", { text: "14" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "None12/14/2018(Date)" });
});
test("rendering of tracked field of type date: from a set date to no date", async () => {
mockDate("2018-12-01");
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
date_field: "2018-12-14",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=date_field] button");
await insertText("div[name=date_field] input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "12/14/2018None(Date)" });
});
test("rendering of tracked field of type datetime: from no date and time to a set date and time", async function () {
mockDate("2018-12-01", 3);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
datetime_field: false,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=datetime_field] input");
await click(".o_datetime_button", { text: "14" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "None12/14/2018 12:00:00(Datetime)" });
const [savedRecord] = pyEnv["mail.test.track.all"].search_read([
["id", "=", mailTestTrackAllId1],
]);
expect(savedRecord.datetime_field).toBe("2018-12-14 09:00:00");
});
test("rendering of tracked field of type datetime: from a set date and time to no date and time", async () => {
mockTimeZone(3);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
datetime_field: "2018-12-14 13:42:28 ",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("div[name=datetime_field] button");
await insertText("div[name=datetime_field] input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "12/14/2018 16:42:28None(Datetime)" });
});
test("rendering of tracked field of type text: from some text to empty", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
text_field: "Marc",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=text_field] textarea", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "MarcNone(Text)" });
});
test("rendering of tracked field of type text: from empty to some text", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
text_field: "",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText("div[name=text_field] textarea", "Marc", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Text)" });
});
test("rendering of tracked field of type selection: from a selection to no selection", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
selection_field: "first",
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await editSelectMenu("div[name=selection_field] input", { value: "" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "firstNone(Selection)" });
});
test("rendering of tracked field of type selection: from no selection to a selection", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await editSelectMenu("div[name=selection_field] input", { value: "First" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "Nonefirst(Selection)" });
});
test("rendering of tracked field of type many2one: from having a related record to no related record", async () => {
const pyEnv = await startServer();
const resPartnerId1 = pyEnv["res.partner"].create({ name: "Marc" });
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({
many2one_field_id: resPartnerId1,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await insertText(".o_field_many2one_selection input", "", { replace: true });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "MarcNone(Many2one)" });
});
test("rendering of tracked field of type many2one: from no related record to having a related record", async () => {
const pyEnv = await startServer();
pyEnv["res.partner"].create({ name: "Marc" });
const mailTestTrackAllId1 = pyEnv["mail.test.track.all"].create({});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId1);
await click("[name=many2one_field_id] input");
await click("[name=many2one_field_id] .o-autocomplete--dropdown-item", { text: "Marc" });
await click(".o_form_button_save");
await contains(".o-mail-Message-tracking", { text: "NoneMarc(Many2one)" });
});
test("Search message with filter in chatter", async () => {
const pyEnv = await startServer();
const mailTestTrackAllId = pyEnv["mail.test.track.all"].create({});
pyEnv["mail.message"].create({
body: "Hermit",
model: "mail.test.track.all",
res_id: mailTestTrackAllId,
});
await start();
registerArchs(archs);
await openFormView("mail.test.track.all", mailTestTrackAllId);
await click("[name=many2one_field_id] input");
await click("[name=many2one_field_id] .o-autocomplete--dropdown-item", { text: "Hermit" });
await click(".o_form_button_save");
// Search message with filter
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "Hermit");
await click("button[title='Filter Messages']");
await click("span", { text: "Conversations" });
await contains(".o-mail-SearchMessageResult .o-mail-Message", { text: "Hermit" });
await click("button[title='Filter Messages']");
await click("span", { text: "Tracked Changes" });
await contains(".o-mail-SearchMessageResult .o-mail-Message", { text: "Hermit" });
await click("button[title='Filter Messages']");
await click("span", { text: "All" });
await contains(".o-mail-SearchMessageResult .o-mail-Message", { count: 2 });
});

View file

@ -1,438 +0,0 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { editInput, editSelect, selectDropdownItem, patchWithCleanup, patchTimeZone } from "@web/../tests/helpers/utils";
import session from 'web.session';
import testUtils from 'web.test_utils';
QUnit.module('test_mail', {}, function () {
QUnit.module('tracking_value_tests.js', {
beforeEach() {
const views = {
'mail.test.track.all,false,form':
`<form>
<sheet>
<field name="boolean_field"/>
<field name="char_field"/>
<field name="date_field"/>
<field name="datetime_field"/>
<field name="float_field"/>
<field name="integer_field"/>
<field name="monetary_field"/>
<field name="many2one_field_id"/>
<field name="selection_field"/>
<field name="text_field"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>`,
};
this.start = async ({ res_id }) => {
const { openFormView, ...remainder } = await start({
serverData: { views },
});
await openFormView(
{
res_model: 'mail.test.track.all',
res_id,
},
{
props: { mode: 'edit' },
},
);
return remainder;
};
patchWithCleanup(session, {
getTZOffset() {
return 0;
},
});
},
});
QUnit.test('basic rendering of tracking value (float type)', async function (assert) {
assert.expect(8);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 12.30 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=float_field] input', 45.67);
await click('.o_form_button_save');
assert.containsOnce(
document.body,
'.o_TrackingValue',
"should display a tracking value"
);
assert.containsOnce(
document.body,
'.o_TrackingValue_fieldName',
"should display the name of the tracked field"
);
assert.strictEqual(
document.querySelector('.o_TrackingValue_fieldName').textContent,
"(Float)",
"should display the correct tracked field name (Float)",
);
assert.containsOnce(
document.body,
'.o_TrackingValue_oldValue',
"should display the old value"
);
assert.strictEqual(
document.querySelector('.o_TrackingValue_oldValue').textContent,
"12.30",
"should display the correct old value (12.30)",
);
assert.containsOnce(
document.body,
'.o_TrackingValue_separator',
"should display the separator"
);
assert.containsOnce(
document.body,
'.o_TrackingValue_newValue',
"should display the new value"
);
assert.strictEqual(
document.querySelector('.o_TrackingValue_newValue').textContent,
"45.67",
"should display the correct new value (45.67)",
);
});
QUnit.test('rendering of tracked field of type float: from non-0 to 0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=float_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"1.000.00(Float)",
"should display the correct content of tracked field of type float: from non-0 to 0 (1.00 -> 0.00 (Float))"
);
});
QUnit.test('rendering of tracked field of type float: from 0 to non-0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ float_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=float_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"0.001.00(Float)",
"should display the correct content of tracked field of type float: from 0 to non-0 (0.00 -> 1.00 (Float))"
);
});
QUnit.test('rendering of tracked field of type integer: from non-0 to 0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=integer_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"10(Integer)",
"should display the correct content of tracked field of type integer: from non-0 to 0 (1 -> 0 (Integer))"
);
});
QUnit.test('rendering of tracked field of type integer: from 0 to non-0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ integer_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=integer_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"01(Integer)",
"should display the correct content of tracked field of type integer: from 0 to non-0 (0 -> 1 (Integer))"
);
});
QUnit.test('rendering of tracked field of type monetary: from non-0 to 0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=monetary_field] input', 0);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"1.000.00(Monetary)",
"should display the correct content of tracked field of type monetary: from non-0 to 0 (1.00 -> 0.00 (Monetary))"
);
});
QUnit.test('rendering of tracked field of type monetary: from 0 to non-0', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ monetary_field: 0 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=monetary_field] input', 1);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"0.001.00(Monetary)",
"should display the correct content of tracked field of type monetary: from 0 to non-0 (0.00 -> 1.00 (Monetary))"
);
});
QUnit.test('rendering of tracked field of type boolean: from true to false', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ boolean_field: true });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
document.querySelector('.o_field_boolean input').click();
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"YesNo(Boolean)",
"should display the correct content of tracked field of type boolean: from true to false (True -> False (Boolean))"
);
});
QUnit.test('rendering of tracked field of type boolean: from false to true', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
document.querySelector('.o_field_boolean input').click();
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoYes(Boolean)",
"should display the correct content of tracked field of type boolean: from false to true (False -> True (Boolean))"
);
});
QUnit.test('rendering of tracked field of type char: from a string to empty string', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: 'Marc' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=char_field] input', '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"MarcNone(Char)",
"should display the correct content of tracked field of type char: from a string to empty string (Marc -> None (Char))"
);
});
QUnit.test('rendering of tracked field of type char: from empty string to a string', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ char_field: '' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=char_field] input', 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoneMarc(Char)",
"should display the correct content of tracked field of type char: from empty string to a string (None -> Marc (Char))"
);
});
QUnit.test('rendering of tracked field of type date: from no date to a set date', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: false });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '12/14/2018', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"None12/14/2018(Date)",
"should display the correct content of tracked field of type date: from no date to a set date (None -> 12/14/2018 (Date))"
);
});
QUnit.test('rendering of tracked field of type date: from a set date to no date', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ date_field: '2018-12-14' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=date_field] .o_datepicker .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"12/14/2018None(Date)",
"should display the correct content of tracked field of type date: from a set date to no date (12/14/2018 -> None (Date))"
);
});
QUnit.test('rendering of tracked field of type datetime: from no date and time to a set date and time', async function (assert) {
assert.expect(2);
patchTimeZone(180);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: false });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '12/14/2018 13:42:28', ['change']);
await click('.o_form_button_save');
const savedRecord = pyEnv.getData()["mail.test.track.all"].records.find(({id}) => id === mailTestTrackAllId1);
assert.strictEqual(savedRecord.datetime_field, '2018-12-14 10:42:28');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"None12/14/2018 13:42:28(Datetime)",
"should display the correct content of tracked field of type datetime: from no date and time to a set date and time (None -> 12/14/2018 13:42:28 (Datetime))"
);
});
QUnit.test('rendering of tracked field of type datetime: from a set date and time to no date and time', async function (assert) {
assert.expect(1);
patchTimeZone(180)
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ datetime_field: '2018-12-14 13:42:28 ' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(document.querySelector('div[name=datetime_field] .o_datepicker .o_datepicker_input'), '', ['change']);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"12/14/2018 16:42:28None(Datetime)",
"should display the correct content of tracked field of type datetime: from a set date and time to no date and time (12/14/2018 13:42:28 -> None (Datetime))"
);
});
QUnit.test('rendering of tracked field of type text: from some text to empty', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: 'Marc' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=text_field] textarea', '');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"MarcNone(Text)",
"should display the correct content of tracked field of type text: from some text to empty (Marc -> None (Text))"
);
});
QUnit.test('rendering of tracked field of type text: from empty to some text', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ text_field: '' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, 'div[name=text_field] textarea', 'Marc');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoneMarc(Text)",
"should display the correct content of tracked field of type text: from empty to some text (None -> Marc (Text))"
);
});
QUnit.test('rendering of tracked field of type selection: from a selection to no selection', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ selection_field: 'first' });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editSelect(document.body, 'div[name=selection_field] select', false);
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"firstNone(Selection)",
"should display the correct content of tracked field of type selection: from a selection to no selection (first -> None (Selection))"
);
});
QUnit.test('rendering of tracked field of type selection: from no selection to a selection', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editSelect(document.body, 'div[name=selection_field] select', '"first"');
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"Nonefirst(Selection)",
"should display the correct content of tracked field of type selection: from no selection to a selection (None -> first (Selection))"
);
});
QUnit.test('rendering of tracked field of type many2one: from having a related record to no related record', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Marc' });
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({ many2one_field_id: resPartnerId1 });
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await editInput(document.body, ".o_field_many2one_selection input", '')
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"MarcNone(Many2one)",
"should display the correct content of tracked field of type many2one: from having a related record to no related record (Marc -> None (Many2one))"
);
});
QUnit.test('rendering of tracked field of type many2one: from no related record to having a related record', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['res.partner'].create({ display_name: 'Marc' });
const mailTestTrackAllId1 = pyEnv['mail.test.track.all'].create({});
const { click } = await this.start({ res_id: mailTestTrackAllId1 });
await selectDropdownItem(document.body, "many2one_field_id", "Marc")
await click('.o_form_button_save');
assert.strictEqual(
document.querySelector('.o_TrackingValue').textContent,
"NoneMarc(Many2one)",
"should display the correct content of tracked field of type many2one: from no related record to having a related record (None -> Marc (Many2one))"
);
});
});