19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:34 +01:00
parent 5faf7397c5
commit 2696f14ed7
721 changed files with 220375 additions and 91221 deletions

View file

@ -0,0 +1,75 @@
import { defineCalendarModels } from "@calendar/../tests/calendar_test_helpers";
import {
click,
contains,
openFormView,
registerArchs,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { test } from "@odoo/hoot";
import { preloadBundle } from "@web/../tests/web_test_helpers";
defineCalendarModels();
preloadBundle("web.fullcalendar_lib");
test("activity click on Reschedule", async () => {
registerArchs({ "calendar.event,false,calendar": `<calendar date_start="start"/>` });
const pyEnv = await startServer();
const resPartnerId = pyEnv["res.partner"].create({});
const meetingActivityTypeId = pyEnv["mail.activity.type"].create({
icon: "fa-calendar",
name: "Meeting",
});
const calendarAttendeeId = pyEnv["calendar.attendee"].create({
partner_id: resPartnerId,
});
const calendaMeetingId = pyEnv["calendar.event"].create({
res_model: "calendar.event",
name: "meeting1",
start: "2022-07-06 06:30:00",
attendee_ids: [calendarAttendeeId],
});
pyEnv["mail.activity"].create({
name: "Small Meeting",
activity_type_id: meetingActivityTypeId,
can_write: true,
res_id: resPartnerId,
res_model: "res.partner",
calendar_event_id: calendaMeetingId,
});
await start();
await openFormView("res.partner", resPartnerId);
await click(".btn", { text: "Reschedule" });
await contains(".o_calendar_view");
});
test("Can cancel activity linked to an event", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Milan Kundera" });
const activityTypeId = pyEnv["mail.activity.type"].create({
icon: "fa-calendar",
name: "Meeting",
});
const attendeeId = pyEnv["calendar.attendee"].create({
partner_id: partnerId,
});
const calendaMeetingId = pyEnv["calendar.event"].create({
res_model: "calendar.event",
name: "meeting1",
start: "2022-07-06 06:30:00",
attendee_ids: [attendeeId],
});
pyEnv["mail.activity"].create({
name: "Small Meeting",
activity_type_id: activityTypeId,
can_write: true,
res_id: partnerId,
res_model: "res.partner",
calendar_event_id: calendaMeetingId,
});
await start();
await openFormView("res.partner", partnerId);
await click(".o-mail-Activity .btn", { text: "Cancel" });
await contains(".o-mail-Activity", { count: 0 });
});

View file

@ -0,0 +1,50 @@
import { defineCalendarModels } from "@calendar/../tests/calendar_test_helpers";
import { click, contains, start, startServer } from "@mail/../tests/mail_test_helpers";
import { test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import {
asyncStep,
mockService,
preloadBundle,
serverState,
waitForSteps,
} from "@web/../tests/web_test_helpers";
defineCalendarModels();
preloadBundle("web.fullcalendar_lib");
test("activity menu widget:today meetings", async () => {
mockDate(2018, 3, 20, 6, 0, 0);
const pyEnv = await startServer();
const attendeeId = pyEnv["calendar.attendee"].create({ partner_id: serverState.partnerId });
pyEnv["calendar.event"].create([
{
res_model: "calendar.event",
name: "meeting1",
start: "2018-04-20 06:30:00",
attendee_ids: [attendeeId],
},
{
res_model: "calendar.event",
name: "meeting2",
start: "2018-04-20 09:30:00",
attendee_ids: [attendeeId],
},
]);
mockService("action", {
doAction(action) {
if (typeof action === "string") {
asyncStep(action);
}
},
});
await start();
await contains(".o_menu_systray i[aria-label='Activities']");
await click(".o_menu_systray i[aria-label='Activities']");
await contains(".o-mail-ActivityGroup div[name='activityTitle']", { text: "Today's Meetings" });
await contains(".o-mail-ActivityGroup .o-calendar-meeting", { count: 2 });
await contains(".o-calendar-meeting span.fw-bold", { text: "meeting1" });
await contains(".o-calendar-meeting span:not(.fw-bold)", { text: "meeting2" });
await click(".o-mail-ActivityMenu .o-mail-ActivityGroup");
await waitForSteps(["calendar.action_calendar_event"]);
});

View file

@ -1,49 +0,0 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('calendar', () => {
QUnit.module('components', () => {
QUnit.module('activity_tests.js');
QUnit.test('activity click on Reschedule', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId = pyEnv['res.partner'].create({});
const meetingActivityTypeId = pyEnv['mail.activity.type'].create({ icon: 'fa-calendar', name: "Meeting" });
const calendarAttendeeId = pyEnv['calendar.attendee'].create({ partner_id: resPartnerId });
const calendaMeetingId = pyEnv['calendar.event'].create({
res_model: "calendar.event",
name: "meeting1",
start: "2022-07-06 06:30:00",
attendee_ids: [calendarAttendeeId],
});
pyEnv['mail.activity'].create({
name: "Small Meeting",
activity_type_id: meetingActivityTypeId,
can_write: true,
res_id: resPartnerId,
res_model: 'res.partner',
calendar_event_id: calendaMeetingId,
});
const { click, openFormView } = await start();
await openFormView(
{
res_model: 'res.partner',
res_id: resPartnerId,
},
);
await click('.o_Activity_editButton');
assert.containsOnce(
document.body,
'.o_calendar_view',
"should have opened calendar view"
);
});
});
});

View file

@ -0,0 +1,63 @@
import { defineCalendarModels } from "@calendar/../tests/calendar_test_helpers";
import {
click,
contains,
openListView,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { test } from "@odoo/hoot";
import { preloadBundle, serverState } from "@web/../tests/web_test_helpers";
import { serializeDateTime } from "@web/core/l10n/dates";
const { DateTime } = luxon;
defineCalendarModels();
preloadBundle("web.fullcalendar_lib");
test("list activity widget: reschedule button in dropdown", async () => {
const pyEnv = await startServer();
const resPartnerId = pyEnv["res.partner"].create({});
const activityTypeId = pyEnv["mail.activity.type"].create({
icon: "fa-calendar",
name: "Meeting",
});
const tomorrow = serializeDateTime(DateTime.now().plus({ days: 1 }));
const attendeeId = pyEnv["calendar.attendee"].create({ partner_id: resPartnerId });
const meetingId = pyEnv["calendar.event"].create({
res_model: "calendar.event",
name: "meeting1",
start: tomorrow,
attendee_ids: [attendeeId],
});
const activityId_1 = pyEnv["mail.activity"].create({
name: "OXP",
activity_type_id: activityTypeId,
date_deadline: tomorrow,
state: "planned",
can_write: true,
res_id: resPartnerId,
res_model: "res.partner",
calendar_event_id: meetingId,
summary: "OXP",
});
pyEnv["res.partner"].write([serverState.partnerId], {
activity_ids: [activityId_1],
activity_state: "today",
});
// FIXME: Manually trigger recomputation of related fields
pyEnv["res.users"]._applyComputesAndValidate();
pyEnv["res.users"][0].activity_ids = [activityId_1];
await start();
await openListView("res.users", {
arch: `<list>
<field name="activity_ids" widget="list_activity"/>
</list>`,
});
await contains(".o-mail-ListActivity-summary", { text: "OXP" });
await click(".o-mail-ActivityButton"); // open the popover
await contains(".o-mail-ActivityListPopoverItem-editbtn .fa-pencil", { count: 0 });
await contains(".o-mail-ActivityListPopoverItem-editbtn .fa-calendar");
});

View file

@ -0,0 +1,147 @@
import { defineCalendarModels } from "@calendar/../tests/calendar_test_helpers";
import { beforeEach, expect, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import {
contains,
makeMockServer,
MockServer,
mountView,
onRpc,
preloadBundle,
serverState,
} from "@web/../tests/web_test_helpers";
import {
changeScale,
clickEvent,
expandCalendarView,
findDateColumn,
findTimeRow,
} from "@web/../tests/views/calendar/calendar_test_helpers";
defineCalendarModels();
preloadBundle("web.fullcalendar_lib");
const serverData = {};
const arch = /*xml*/ `
<calendar js_class="attendee_calendar"
event_open_popup="1"
date_start="start"
date_stop="stop"
all_day="allday"
mode="month"
>
<field name="partner_ids" options="{'block': True, 'icon': 'fa fa-users'}"
filters="1" widget="many2manyattendeeexpandable" write_model="calendar.filters"
write_field="partner_id" filter_field="partner_checked" avatar_field="avatar_128"/>
<field name="partner_id" string="Organizer" options="{'icon': 'fa fa-user-o'}"/>
<field name="user_id"/>
<field name="start"/>
<field name="stop"/>
<field name="allday"/>
<field name="res_model_name" invisible="not res_model_name"
options="{'icon': 'fa fa-link', 'shouldOpenRecord': true}"/>
</calendar>
`;
async function selectTimeStart(startDateTime) {
const [startDate, startTime] = startDateTime.split(" ");
const startCol = findDateColumn(startDate);
const startRow = findTimeRow(startTime);
await scrollTo(startRow);
const startColRect = startCol.getBoundingClientRect();
const startRowRect = startRow.getBoundingClientRect();
await contains(startRow).click({
position: {
x: startColRect.x + startColRect.width / 2,
y: startRowRect.y + 1,
},
});
}
beforeEach(async () => {
mockDate("2016-12-12 08:00:00", 0);
const { env: pyEnv } = await makeMockServer();
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{ name: "Partner 1" },
{ name: "Partner 2" },
]);
serverData.partnerId_1 = partnerId_1;
serverData.partnerId_2 = partnerId_2;
serverData.userId = pyEnv["res.users"].create({ name: "User 1", partner_id: partnerId_1 });
serverData.attendeeIds = pyEnv["calendar.attendee"].create([
{ partner_id: serverState.partnerId },
{ partner_id: partnerId_1 },
{ partner_id: partnerId_2 },
]);
pyEnv["calendar.filters"].create([
{ partner_id: partnerId_1, partner_checked: true, user_id: serverState.userId },
{ partner_id: partnerId_2, partner_checked: true, user_id: serverData.userId },
]);
pyEnv["calendar.event"].create([
{
name: "event 1",
start: "2016-12-11 00:00:00",
stop: "2016-12-11 01:00:00",
attendee_ids: serverData.attendeeIds,
partner_ids: [serverState.partnerId, partnerId_1, partnerId_2],
},
{
name: "event 2",
start: "2016-12-12 10:55:05",
stop: "2016-12-12 14:55:05",
attendee_ids: [serverData.attendeeIds[0], serverData.attendeeIds[1]],
partner_ids: [serverState.partnerId, partnerId_1],
},
]);
});
test("Linked record rendering", async () => {
const pyEnv = MockServer.current.env;
onRpc("res.users", "has_group", () => true);
onRpc("res.users", "check_synchronization_status", () => ({}));
onRpc("res.partner", "get_attendee_detail", () => []);
onRpc("/calendar/check_credentials", () => ({}));
const { id: modelId, display_name } = pyEnv["ir.model"].search_read(
[["model", "=", "res.partner"]],
["display_name"]
)[0];
const eventId = pyEnv["calendar.event"].create({
user_id: serverData.userId,
name: "event With record",
start: "2016-12-11 09:00:00",
stop: "2016-12-11 10:00:00",
attendee_ids: serverData.attendeeIds,
partner_ids: [serverState.partnerId, serverData.partnerId_1, serverData.partnerId_2],
res_model_id: modelId,
});
await mountView({ type: "calendar", resModel: "calendar.event", arch });
expect(".o_calendar_renderer .fc-view").toHaveCount(1);
await changeScale("week");
await clickEvent(eventId);
expect(".fa-link").toHaveCount(1, { message: "A link icon should be present" });
expect("li a[href='#']").toHaveText(display_name);
});
test("Default duration rendering", async () => {
onRpc("res.users", "has_group", () => true);
onRpc("res.users", "check_synchronization_status", () => ({}));
onRpc("res.partner", "get_attendee_detail", () => []);
onRpc("/calendar/check_credentials", () => ({}));
await mountView({ type: "calendar", resModel: "calendar.event", arch });
expandCalendarView();
await changeScale("week");
await selectTimeStart("2016-12-15 15:00:00");
await contains(".o-calendar-quick-create--input").edit("Event with new duration", {
confirm: false,
});
await contains(".o-calendar-quick-create--create-btn").click();
// This new event is the third
await clickEvent(3);
expect("div[name='start'] div").toHaveText("Dec 15, 3:00 PM");
expect("div[name='stop'] div").toHaveText("Dec 15, 6:15 PM", {
message: "The duration should be 3.25 hours",
});
});

View file

@ -0,0 +1,79 @@
import { defineCalendarModels } from "@calendar/../tests/calendar_test_helpers";
import { beforeEach, expect, test } from "@odoo/hoot";
import { contains, makeMockServer, mountView, onRpc } from "@web/../tests/web_test_helpers";
import { getOrigin } from "@web/core/utils/urls";
defineCalendarModels();
const serverData = {};
beforeEach(async () => {
const { env: pyEnv } = await makeMockServer();
serverData.partnerIds = pyEnv["res.partner"].create([{ name: "Zeus" }, { name: "Azdaha" }]);
serverData.eventId = pyEnv["calendar.event"].create({
name: "event 1",
partner_ids: serverData.partnerIds,
});
});
test("Many2ManyAttendee: basic rendering", async () => {
onRpc("get_attendee_detail", (request) => {
expect.step("get_attendee_detail");
expect(request.model).toBe("res.partner");
expect(request.args[0]).toEqual(serverData.partnerIds);
expect(request.args[1]).toEqual([serverData.eventId]);
return [
{ id: serverData.partnerIds[0], name: "Zeus", status: "accepted", color: 0 },
{ id: serverData.partnerIds[1], name: "Azdaha", status: "tentative", color: 0 },
];
});
await mountView({
type: "form",
resModel: "calendar.event",
resId: serverData.eventId,
arch: /*xml*/ `
<form>
<field name="partner_ids" widget="many2manyattendee"/>
</form>
`,
});
expect(".o_field_widget[name='partner_ids'] div.o_field_tags").toHaveCount(1);
expect(".o_field_widget[name='partner_ids'] .o_tag").toHaveCount(2);
expect(".o_field_widget[name='partner_ids'] .o_tag:eq(0)").toHaveText("Zeus");
expect(
".o_field_widget[name='partner_ids'] .o_tag:eq(0) .attendee_tag_status.o_attendee_status_accepted"
).toHaveCount(1);
expect(".o_field_widget[name='partner_ids'] .o_tag:eq(1)").toHaveText("Azdaha");
expect(
".o_field_widget[name='partner_ids'] .o_tag:eq(1) .attendee_tag_status.o_attendee_status_tentative"
).toHaveCount(1);
expect(".o_field_widget[name='partner_ids'] .o_tag:eq(0) img").toHaveCount(1);
expect(".o_field_widget[name='partner_ids'] .o_tag:eq(0) img").toHaveAttribute(
"data-src",
`${getOrigin()}/web/image/res.partner/${serverData.partnerIds[0]}/avatar_128`
);
expect.verifySteps(["get_attendee_detail"]);
});
test("Many2ManyAttendee: remove own attendee", async () => {
onRpc("get_attendee_detail", () => [
{ id: serverData.partnerIds[0], name: "Zeus", status: "accepted", color: 0 },
{ id: serverData.partnerIds[1], name: "Azdaha", status: "tentative", color: 0 },
]);
await mountView({
type: "form",
resModel: "calendar.event",
resId: serverData.eventId,
arch: /*xml*/ `
<form>
<field name="partner_ids" widget="many2manyattendee"/>
</form>
`,
});
expect(".o_field_widget[name='partner_ids'] .o_tag").toHaveCount(2);
// Attendee must be able to uninvite itself from the event.
await contains(".o_field_widget[name='partner_ids'] .o_delete", { visible: false }).click();
await contains(".o_form_button_save").click();
expect(".o_field_widget[name='partner_ids'] .o_tag").toHaveCount(1);
});

View file

@ -0,0 +1,78 @@
import { defineCalendarModels } from "@calendar/../tests/calendar_test_helpers";
import { click, contains, start, startServer } from "@mail/../tests/mail_test_helpers";
import { test } from "@odoo/hoot";
import {
asyncStep,
mockService,
onRpc,
preloadBundle,
serverState,
waitForSteps,
} from "@web/../tests/web_test_helpers";
defineCalendarModels();
preloadBundle("web.fullcalendar_lib");
test("can listen on bus and display notifications in DOM and click OK", async () => {
const pyEnv = await startServer();
onRpc("/calendar/notify_ack", () => asyncStep("notify_ack"));
await start();
pyEnv["bus.bus"]._sendone(serverState.partnerId, "calendar.alarm", [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 0,
notify_at: "1978-04-14 12:45:00",
},
]);
await contains(".o_notification", { text: "Meeting. Very old meeting message" });
await click(".o_notification_buttons button", { text: "OK" });
await contains(".o_notification", { count: 0 });
await waitForSteps(["notify_ack"]);
});
test("can listen on bus and display notifications in DOM and click Detail", async () => {
mockService("action", {
doAction(actionId) {
asyncStep(actionId.type);
},
});
const pyEnv = await startServer();
await start();
pyEnv["bus.bus"]._sendone(serverState.partnerId, "calendar.alarm", [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 0,
notify_at: "1978-04-14 12:45:00",
},
]);
await contains(".o_notification", { text: "Meeting. Very old meeting message" });
await click(".o_notification_buttons button", { text: "Details" });
await contains(".o_notification", { count: 0 });
await waitForSteps(["ir.actions.act_window"]);
});
test("can listen on bus and display notifications in DOM and click Snooze", async () => {
const pyEnv = await startServer();
onRpc("/calendar/notify_ack", () => asyncStep("notify_ack"));
await start();
pyEnv["bus.bus"]._sendone(serverState.partnerId, "calendar.alarm", [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 0,
notify_at: "1978-04-14 12:45:00",
},
]);
await contains(".o_notification", { text: "Meeting. Very old meeting message" });
await click(".o_notification button", { text: "Snooze" });
await contains(".o_notification", { count: 0 });
await waitForSteps([]);
});

View file

@ -1,120 +0,0 @@
/** @odoo-module */
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service";
import { click, contains } from "@web/../tests/utils";
import { registry } from "@web/core/registry";
import { start } from "@mail/../tests/helpers/test_utils";
const serviceRegistry = registry.category("services");
QUnit.module("Calendar Notification", (hooks) => {
hooks.beforeEach(() => {
serviceRegistry.add("calendarNotification", calendarNotificationService);
});
QUnit.test(
"can listen on bus and display notifications in DOM and click OK",
async (assert) => {
const pyEnv = await startServer();
const mockRPC = (route, args) => {
if (route === "/calendar/notify") {
return Promise.resolve([]);
}
if (route === "/calendar/notify_ack") {
assert.step("notifyAck");
return Promise.resolve(true);
}
};
await start({ mockRPC });
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "calendar.alarm", [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 0,
notify_at: "1978-04-14 12:45:00",
},
]);
await contains(".o_notification", { text: "Very old meeting message" });
await click(".o_notification_buttons button", { text: "OK" });
await contains(".o_notification", { count: 0 });
assert.verifySteps(["notifyAck"]);
}
);
QUnit.test(
"can listen on bus and display notifications in DOM and click Detail",
async (assert) => {
const pyEnv = await startServer();
const mockRPC = (route, args) => {
if (route === "/calendar/notify") {
return Promise.resolve([]);
}
};
const fakeActionService = {
name: "action",
start() {
return {
doAction(actionId) {
assert.step(actionId.type);
return Promise.resolve(true);
},
loadState(state, options) {
return Promise.resolve(true);
},
};
},
};
serviceRegistry.add("action", fakeActionService, { force: true });
await start({ mockRPC });
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "calendar.alarm", [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 0,
notify_at: "1978-04-14 12:45:00",
},
]);
await contains(".o_notification", { text: "Very old meeting message" });
await click(".o_notification_buttons button", { text: "Details" });
await contains(".o_notification", { count: 0 });
assert.verifySteps(["ir.actions.act_window"]);
}
);
QUnit.test(
"can listen on bus and display notifications in DOM and click Snooze",
async (assert) => {
const pyEnv = await startServer();
const mockRPC = (route, args) => {
if (route === "/calendar/notify") {
return Promise.resolve([]);
}
if (route === "/calendar/notify_ack") {
assert.step("notifyAck");
return Promise.resolve(true);
}
};
await start({ mockRPC });
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "calendar.alarm", [
{
alarm_id: 1,
event_id: 2,
title: "Meeting",
message: "Very old meeting message",
timer: 0,
notify_at: "1978-04-14 12:45:00",
},
]);
await contains(".o_notification", { text: "Very old meeting message" });
await click(".o_notification button", { text: "Snooze" });
await contains(".o_notification", { count: 0 });
assert.verifySteps([], "should only close the notification withtout calling a rpc");
}
);
});

View file

@ -0,0 +1,20 @@
import { CalendarEvent } from "./mock_server/mock_models/calendar_event";
import { CalendarAttendee } from "./mock_server/mock_models/calendar_attendee";
import { ResUsers } from "./mock_server/mock_models/res_users";
import { MailActivity } from "./mock_server/mock_models/mail_activity";
import { CalendarFilters } from "./mock_server/mock_models/calendar_filters";
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { defineModels } from "@web/../tests/web_test_helpers";
export const calendarModels = {
CalendarAttendee,
CalendarEvent,
CalendarFilters,
ResUsers,
MailActivity,
};
export function defineCalendarModels() {
return defineModels({ ...mailModels, ...calendarModels });
}

View file

@ -1,129 +0,0 @@
/** @odoo-module **/
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module(
"calendar",
{
beforeEach: function () {
target = getFixture();
serverData = {
models: {
event: {
fields: {
partner_ids: {
string: "Partners",
type: "many2many",
relation: "partner",
},
},
records: [
{
id: 14,
partner_ids: [1, 2],
},
],
},
partner: {
fields: {
name: { string: "Name", type: "char" },
},
records: [
{
id: 1,
name: "Jesus",
},
{
id: 2,
name: "Mahomet",
},
],
},
},
};
setupViewRegistries();
},
},
function () {
QUnit.test("Many2ManyAttendee: basic rendering", async function (assert) {
await makeView({
type: "form",
resModel: "event",
serverData,
resId: 14,
arch: `
<form>
<field name="partner_ids" widget="many2manyattendee"/>
</form>`,
mockRPC(route, args) {
if (args.method === "get_attendee_detail") {
assert.step(args.method);
assert.strictEqual(
args.model,
"res.partner",
"the method should only be called on res.partner"
);
assert.deepEqual(
args.args[0],
[1, 2],
"the partner ids should be passed as argument"
);
assert.deepEqual(
args.args[1],
[14],
"the event id should be passed as argument"
);
return Promise.resolve([
{ id: 1, name: "Jesus", status: "accepted", color: 0 },
{ id: 2, name: "Mahomet", status: "tentative", color: 0 },
]);
}
},
});
assert.hasClass(
target.querySelector('.o_field_widget[name="partner_ids"] div'),
"o_field_tags"
);
assert.containsN(
target,
'.o_field_widget[name="partner_ids"] .badge',
2,
"there should be 2 tags"
);
const badges = target.querySelectorAll('.o_field_widget[name="partner_ids"] .badge');
assert.strictEqual(
badges[0].textContent.trim(),
"Jesus",
"the tag should be correctly named"
);
assert.hasClass(
badges[0].querySelector("img"),
"o_attendee_border_accepted",
"Jesus should attend the meeting"
);
assert.strictEqual(
badges[1].textContent.trim(),
"Mahomet",
"the tag should be correctly named"
);
assert.hasClass(
badges[1].querySelector("img"),
"o_attendee_border_tentative",
"Mohamet should still confirm his attendance to the meeting"
);
assert.containsOnce(badges[0], "img", "should have img tag");
assert.hasAttrValue(
badges[0].querySelector("img"),
"data-src",
"/web/image/partner/1/avatar_128",
"should have correct avatar image"
);
assert.verifySteps(["get_attendee_detail"]);
});
}
);

View file

@ -1,93 +0,0 @@
/** @odoo-module **/
// ensure mail override is applied first.
import '@mail/../tests/helpers/mock_server';
import { patch } from '@web/core/utils/patch';
import { MockServer } from '@web/../tests/helpers/mock_server';
import { datetime_to_str } from 'web.time';
patch(MockServer.prototype, 'calendar', {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
async _performRPC(route, args) {
// mail.activity methods
if (args.model === 'mail.activity' && args.method === 'action_create_calendar_event') {
return {
type: 'ir.actions.act_window',
name: "Meetings",
res_model: 'calendar.event',
view_mode: 'calendar',
views: [[false, 'calendar']],
target: 'current',
};
}
// calendar.event methods
if (args.model === 'calendar.event' && args.method === 'check_access_rights') {
return true;
}
return this._super(...arguments);
},
//--------------------------------------------------------------------------
// Private Mocked Methods
//--------------------------------------------------------------------------
/**
* Simulates `_systray_get_calendar_event_domain` on `res.users`.
*
* @private
*/
_mockResUsers_SystrayGetCalendarEventDomain() {
const startDate = new Date();
startDate.setUTCHours(0, 0, 0, 0);
const endDate = new Date();
endDate.setUTCHours(23, 59, 59, 999);
const currentPartnerAttendeeIds = this.pyEnv['calendar.attendee'].search([['partner_id', '=', this.currentPartnerId]]);
return [
'&',
'|',
'&',
'|',
['start', '>=', datetime_to_str(startDate)],
['stop', '>=', datetime_to_str(startDate)],
['start', '<=', datetime_to_str(endDate)],
'&',
['allday', '=', true],
['start_date', '=', datetime_to_str(startDate)],
['attendee_ids', 'in', currentPartnerAttendeeIds],
];
},
/**
* Simulates `systray_get_activities` on `res.users`.
*
* @override
*/
_mockResUsersSystrayGetActivities() {
const activities = this._super(...arguments);
const meetingsLines = this.pyEnv['calendar.event'].searchRead(
this._mockResUsers_SystrayGetCalendarEventDomain(),
{
fields: ['id', 'start', 'name', 'allday', 'attendee_status'],
order: 'start',
}
).filter(meetingLine => meetingLine['attendee_status'] !== 'declined');
if (meetingsLines.length) {
activities.unshift({
id: 'calendar.event', // for simplicity
meetings: meetingsLines,
model: 'calendar.event',
name: 'Today\'s Meetings',
type: 'meeting',
});
}
return activities;
},
});

View file

@ -0,0 +1,15 @@
import { patch } from "@web/core/utils/patch";
import { MockServer } from '@web/../tests/helpers/mock_server';
patch(MockServer.prototype, {
/**
* @override
*/
async _performRPC(route, args) {
// calendar.event methods
if (args.model === 'calendar.event' && args.method === 'has_access') {
return true;
}
return super._performRPC(...arguments);
},
});

View file

@ -1,5 +0,0 @@
/** @odoo-module **/
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch(['calendar.event', 'calendar.attendee']);

View file

@ -1,8 +0,0 @@
/** @odoo-module **/
import { registry } from '@web/core/registry';
const viewArchsRegistry = registry.category('bus.view.archs');
const calendarArchsRegistry = viewArchsRegistry.category('calendar');
calendarArchsRegistry.add('default', '<calendar date_start="start"/>');

View file

@ -1,129 +0,0 @@
/** @odoo-module **/
import FormView from "web.FormView";
import testUtils from "web.test_utils";
var createView = testUtils.createView;
QUnit.module(
"Legacy calendar",
{
beforeEach: function () {
this.data = {
event: {
fields: {
partner_ids: { string: "Partners", type: "many2many", relation: "partner" },
},
records: [
{
id: 14,
partner_ids: [1, 2],
},
],
},
partner: {
fields: {
name: { string: "Name", type: "char" },
},
records: [
{
id: 1,
name: "Jesus",
},
{
id: 2,
name: "Mahomet",
},
],
},
};
},
},
function () {
QUnit.test("many2manyattendee widget: basic rendering", async function (assert) {
assert.expect(12);
var form = await createView({
View: FormView,
model: "event",
data: this.data,
res_id: 14,
debug: 1,
arch:
"<form>" + '<field name="partner_ids" widget="many2manyattendee"/>' + "</form>",
mockRPC: function (route, args) {
if (args.method === "get_attendee_detail") {
assert.strictEqual(
args.model,
"res.partner",
"the method should only be called on res.partner"
);
assert.deepEqual(
args.args[0],
[1, 2],
"the partner ids should be passed as argument"
);
assert.deepEqual(
args.args[1],
[14],
"the event id should be passed as argument"
);
return Promise.resolve([
{ id: 1, name: "Jesus", status: "accepted", color: 0 },
{ id: 2, name: "Mahomet", status: "tentative", color: 0 },
]);
}
return this._super.apply(this, arguments);
},
});
assert.hasClass(form.$('.o_field_widget[name="partner_ids"]'), "o_field_many2manytags");
assert.containsN(
form,
'.o_field_widget[name="partner_ids"] .badge',
2,
"there should be 2 tags"
);
assert.strictEqual(
form.$('.o_field_widget[name="partner_ids"] .badge:first').text().trim(),
"Jesus",
"the tag should be correctly named"
);
assert.hasClass(
form.$('.o_field_widget[name="partner_ids"] .badge:first img'),
"o_attendee_border_accepted",
"Jesus should attend the meeting"
);
assert.strictEqual(
form.$('.o_field_widget[name="partner_ids"] .badge[data-id="2"]').text().trim(),
"Mahomet",
"the tag should be correctly named"
);
assert.hasClass(
form.el.querySelector(
'.o_field_widget[name="partner_ids"] .badge[data-id="2"] img'
),
"o_attendee_border_tentative",
"Mohamet should still confirm his attendance to the meeting"
);
assert.hasClass(
form.el.querySelector(".o_field_many2manytags"),
"avatar",
"should have avatar class"
);
assert.containsOnce(
form,
".o_field_many2manytags.avatar.o_field_widget .badge:first img",
"should have img tag"
);
assert.hasAttrValue(
form.$(".o_field_many2manytags.avatar.o_field_widget .badge:first img"),
"data-src",
"/web/image/partner/1/avatar_128",
"should have correct avatar image"
);
form.destroy();
});
}
);

View file

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

View file

@ -0,0 +1,17 @@
import { models, fields, serverState } from "@web/../tests/web_test_helpers";
export class CalendarEvent extends models.ServerModel {
_name = "calendar.event";
user_id = fields.Generic({ default: serverState.userId });
partner_id = fields.Generic({ default: serverState.partnerId });
partner_ids = fields.Generic({ default: [[6, 0, [serverState.partnerId]]] });
has_access() {
return true;
}
get_default_duration() {
return 3.25;
}
}

View file

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

View file

@ -0,0 +1,39 @@
import { mailModels, openView } from "@mail/../tests/mail_test_helpers";
import { fields } from "@web/../tests/web_test_helpers";
export class MailActivity extends mailModels.MailActivity {
name = fields.Char();
async action_create_calendar_event() {
await openView({
res_model: "calendar.event",
views: [[false, "calendar"]],
});
return {
type: "ir.actions.act_window",
name: "Meetings",
res_model: "calendar.event",
view_mode: "calendar",
views: [[false, "calendar"]],
target: "current",
};
}
unlink_w_meeting() {
const eventIds = this.map((act) => act.calendar_event_id);
const res = this.unlink(arguments[0]);
this.env["calendar.event"].unlink(eventIds);
return res;
}
/** @param {number[]} ids */
_to_store(store) {
super._to_store(...arguments);
for (const activity of this) {
if (activity.calendar_event_id) {
store._add_record_fields(this.browse(activity.id), {
calendar_event_id: activity.calendar_event_id,
});
}
}
}
}

View file

@ -0,0 +1,67 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { serverState } from "@web/../tests/web_test_helpers";
import { serializeDateTime } from "@web/core/l10n/dates";
const { DateTime } = luxon;
export class ResUsers extends mailModels.ResUsers {
/**
* Simulates `_systray_get_calendar_event_domain` on `res.users`.
*
* @private
*/
_systray_get_calendar_event_domain() {
const startDate = DateTime.fromObject({
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
});
const endDate = DateTime.fromObject({
hours: 23,
minutes: 59,
seconds: 59,
milliseconds: 999,
});
const currentPartnerAttendeeIds = this.env["calendar.attendee"].search([
["partner_id", "=", serverState.partnerId],
["state", "!=", "declined"],
]);
return [
"&",
"|",
"&",
"|",
["start", ">=", serializeDateTime(startDate)],
["stop", ">=", serializeDateTime(startDate)],
["start", "<=", serializeDateTime(endDate)],
// FIXME: Makes "activity_menu.test.js" fail
// "&",
// ["allday", "=", true],
// ["start_date", "=", serializeDateTime(startDate)],
["attendee_ids", "in", [...currentPartnerAttendeeIds]],
];
}
/** @override */
_get_activity_groups() {
const activities = super._get_activity_groups();
const meetingsLines = this.env["calendar.event"].search_read(
this._systray_get_calendar_event_domain(),
{
fields: ["id", "start", "name", "allday"],
order: "start",
}
);
if (meetingsLines.length) {
activities.unshift({
id: "calendar.event", // for simplicity
meetings: meetingsLines,
model: "calendar.event",
name: "Today's Meetings",
type: "meeting",
});
}
return activities;
}
}

View file

@ -1,48 +0,0 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
QUnit.module('calendar', {}, function () {
QUnit.module('ActivityMenu');
QUnit.test('activity menu widget:today meetings', async function (assert) {
assert.expect(6);
patchDate(2018, 3, 20, 6, 0, 0);
const pyEnv = await startServer();
const calendarAttendeeId1 = pyEnv['calendar.attendee'].create({ partner_id: pyEnv.currentPartnerId });
pyEnv['calendar.event'].create([
{
res_model: "calendar.event",
name: "meeting1",
start: "2018-04-20 06:30:00",
attendee_ids: [calendarAttendeeId1],
},
{
res_model: "calendar.event",
name: "meeting2",
start: "2018-04-20 09:30:00",
attendee_ids: [calendarAttendeeId1],
},
]);
const { click, env } = await start();
assert.containsOnce(document.body, '.o_ActivityMenuView', 'should contain an instance of widget');
await click('.dropdown-toggle[title="Activities"]');
patchWithCleanup(env.services.action, {
doAction(action) {
assert.strictEqual(action, "calendar.action_calendar_event", 'should open meeting calendar view in day mode');
},
});
assert.ok(document.querySelector('.o_meeting_filter'), "should be a meeting");
assert.containsN(document.body, '.o_meeting_filter', 2, 'there should be 2 meetings');
assert.hasClass(document.querySelector('.o_meeting_filter'), 'o_meeting_bold', 'this meeting is yet to start');
assert.doesNotHaveClass(document.querySelectorAll('.o_meeting_filter')[1], 'o_meeting_bold', 'this meeting has been started');
await click('.o_ActivityMenuView_activityGroup');
});
});

View file

@ -1,155 +1,132 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const todayDate = function() {
let now = new Date();
let year = now.getFullYear();
let month = String(now.getMonth() + 1).padStart(2, '0');
let day = String(now.getDate()).padStart(2, '0');
const todayDate = function () {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
return `${month}/${day}/${year} 10:00:00`;
};
tour.register('calendar_appointments_hour_tour', {
url: '/web',
test: true,
}, [
tour.stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="calendar.mail_menu_calendar"]',
content: 'Open Calendar',
run: 'click',
},
{
trigger: '.o-calendar-button-new',
content: 'Create a new event',
run: 'click',
},
{
trigger: '#name',
content: 'Give a name to the new event',
run: 'text TEST EVENT',
},
{
trigger: '#start',
content: 'Give a date to the new event',
run: `text ${todayDate()}`,
},
{
trigger: '.fa-cloud-upload',
content: 'Save the new event',
run: 'click',
},
{
trigger: '.dropdown-item:contains("Calendar")',
content: 'Go back to Calendar view',
run: 'click',
},
{
trigger: '.dropdown-toggle:contains("Week")',
content: 'Click to change calendar view',
run: 'click',
},
{
trigger: '.dropdown-item:contains("Month")',
content: 'Change the calendar view to Month',
run: 'click',
},
{
trigger: '.fc-day-header:contains("Monday")',
content: 'Change the calendar view to week',
},
{
trigger: '.fc-time:contains("10:00")',
content: 'Check the time is properly displayed',
},
{
trigger: '.o_event_title:contains("TEST EVENT")',
content: 'Check the event title',
},
]);
registry.category("web_tour.tours").add("calendar_appointments_hour_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="calendar.mail_menu_calendar"]',
content: "Open Calendar",
run: "click",
},
{
trigger: ".o-calendar-button-new",
content: "Create a new event",
run: "click",
},
{
trigger: "#name_0",
content: "Give a name to the new event",
run: "edit TEST EVENT",
},
{
trigger: "div[name='start'] button",
content: "Open the date picker",
run: "click",
},
{
trigger: "#start_0",
content: "Give a date to the new event",
run: `edit ${todayDate()}`,
},
{
trigger: "#duration_0",
content: "Give a duration to the new event",
run: "edit 02:00",
},
{
trigger: ".fa-cloud-upload",
content: "Save the new event",
run: "click",
},
{
trigger: ".o_back_button",
content: "Go back to Calendar view",
run: "click",
},
{
trigger: ".scale_button_selection",
content: "Click to change calendar view",
run: "click",
},
{
trigger: '.dropdown-item:contains("Month")',
content: "Change the calendar view to Month",
run: "click",
},
{
trigger: ".fc-col-header-cell.fc-day.fc-day-mon",
content: "Check the day is properly displayed",
run: "click",
},
{
trigger: '.fc-time:contains("10:00")',
content: "Check the time is properly displayed",
run: "click",
},
{
trigger: '.o_event_title:contains("TEST EVENT")',
content: "Check the event title",
},
],
});
tour.register('test_calendar_delete_tour', {
test: true,
},
[
{
content: 'Select filter (everybody)',
trigger: 'div[data-value="all"] input',
},
{
content: 'Click on the event (focus + waiting)',
trigger: 'a .fc-content:contains("Test Event")',
async run() {
$('a .fc-content:contains("Test Event")').click();
await new Promise((r) => setTimeout(r, 1000));
$('a .fc-content:contains("Test Event")').click();
const clickOnTheEvent = {
content: "Click on the event (focus + waiting)",
trigger: 'a .fc-event-main:contains("Test Event")',
async run(actions) {
await actions.click();
await new Promise((r) => setTimeout(r, 1000));
const custom = document.querySelector(".o_cw_custom_highlight");
if (custom) {
custom.click();
}
},
{
content: 'Delete the event',
trigger: '.o_cw_popover_delete',
},
{
content: 'Validate the deletion',
trigger:'button:contains("Ok")',
async run() {
$('button:contains("Ok")').click();
await new Promise((r) => setTimeout(r, 1000));
}
},
]);
};
tour.register('test_calendar_decline_tour', {
test: true,
},
[
{
content: 'Click on the event (focus + waiting)',
trigger: 'a .fc-content:contains("Test Event")',
async run() {
$('a .fc-content:contains("Test Event")').click();
await new Promise((r) => setTimeout(r, 1000));
$('a .fc-content:contains("Test Event")').click();
}
},
{
content: 'Delete the event',
trigger: '.o_cw_popover_delete',
},
{
content: 'Wait declined status',
trigger: '.o_attendee_status_declined',
},
]);
registry.category("web_tour.tours").add("test_calendar_delete_tour", {
steps: () => [
clickOnTheEvent,
{
trigger: ".o_cw_popover",
},
{
content: "Delete the event",
trigger: ".o_cw_popover_delete",
run: "click",
},
{
content: "Validate the deletion",
trigger: 'button:contains("Delete")',
run: "click",
},
],
});
tour.register('test_calendar_decline_with_everybody_filter_tour', {
test: true,
},
[
{
content: 'Select filter (everybody)',
trigger: 'div[data-value="all"] input',
},
{
content: 'Click on the event (focus + waiting)',
trigger: 'a .fc-content:contains("Test Event")',
async run() {
$('a .fc-content:contains("Test Event")').click();
await new Promise((r) => setTimeout(r, 1000));
$('a .fc-content:contains("Test Event")').click();
}
},
{
content: 'Delete the event',
trigger: '.o_cw_popover_delete',
},
{
content: 'Select filter (everybody)',
trigger: 'div[data-value="all"] input',
},
{
content: 'Wait declined status',
trigger: '.o_attendee_status_declined',
},
]);
registry.category("web_tour.tours").add("test_calendar_decline_tour", {
steps: () => [
clickOnTheEvent,
{
trigger: ".o_cw_popover",
},
{
content: "Delete the event",
trigger: ".o_cw_popover_delete",
run: "click",
},
{
content: "Wait declined status",
trigger: ".o_attendee_status_declined",
},
],
});