mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 10:12:04 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -0,0 +1,623 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
inputFiles,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { advanceTime, mockDate } from "@odoo/hoot-mock";
|
||||
import {
|
||||
asyncStep,
|
||||
mockService,
|
||||
onRpc,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { deserializeDateTime, serializeDate, today } from "@web/core/l10n/dates";
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("activity upload document is available", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityType = pyEnv["mail.activity.type"].find((r) => r.name === "Upload Document");
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "upload_file",
|
||||
activity_type_id: activityType.id,
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity .btn", { text: "Upload Document" });
|
||||
await contains(".btn .fa-upload");
|
||||
await contains(".o-mail-Activity .o_input_file");
|
||||
});
|
||||
|
||||
test("activity can upload a document", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.partner"].create({});
|
||||
const activityType = pyEnv["mail.activity.type"].find((r) => r.name === "Upload Document");
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "upload_file",
|
||||
activity_type_id: activityType.id,
|
||||
can_write: true,
|
||||
res_id: fakeId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", fakeId, {
|
||||
arch: `
|
||||
<form string="Fake">
|
||||
<sheet></sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o-mail-Activity .btn", { text: "Upload Document" });
|
||||
const file = new File(["hello, world"], "text.txt", { type: "text/plain" });
|
||||
await inputFiles(".o-mail-Activity .o_input_file", [file]);
|
||||
});
|
||||
|
||||
test("activity simplest layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-Activity-sidebar");
|
||||
await contains(".o-mail-Activity-user");
|
||||
await contains(".o-mail-Activity-note", { count: 0 });
|
||||
await contains(".o-mail-Activity-details", { count: 0 });
|
||||
await contains(".o-mail-Activity-mailTemplates", { count: 0 });
|
||||
await contains(".btn", { count: 0, text: "Edit" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Cancel" });
|
||||
await contains(".btn", { count: 0, text: "Mark Done" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Upload Document" });
|
||||
});
|
||||
|
||||
test("activity with note layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
note: "<p>There is no good or bad note</p>",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-Activity-note", { text: "There is no good or bad note" });
|
||||
});
|
||||
|
||||
test("activity info layout when planned after tomorrow", async () => {
|
||||
mockDate("2023-01-11 12:00:00");
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(today().plus({ days: 5 })),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "planned",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity span.text-success", { text: "Due in 5 days:" });
|
||||
});
|
||||
|
||||
test("activity info layout when planned tomorrow", async () => {
|
||||
mockDate("2023-01-11 12:00:00");
|
||||
const tomorrow = today().plus({ days: 1 });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(tomorrow),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "planned",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity span.text-success", { text: "Tomorrow:" });
|
||||
});
|
||||
|
||||
test("activity info layout when planned today", async () => {
|
||||
mockDate("2023-01-11 12:00:00");
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(today()),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "today",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity span.text-warning", { text: "Today:" });
|
||||
});
|
||||
|
||||
test("activity info layout when planned yesterday", async () => {
|
||||
mockDate("2023-01-11 12:00:00");
|
||||
const yesterday = today().plus({ days: -1 });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(yesterday),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "overdue",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity span.text-danger", { text: "Yesterday:" });
|
||||
});
|
||||
|
||||
test("activity info layout when planned before yesterday", async () => {
|
||||
mockDate("2023-01-11 12:00:00");
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(today().plus({ days: -5 })),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "overdue",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity span.text-danger", { text: "5 days overdue:" });
|
||||
});
|
||||
|
||||
test.skip("activity info layout change at midnight", async () => {
|
||||
// skip: does not work consistently both locally and on runbot at the same time (tz issue?)
|
||||
mockDate("2023-12-07 23:59:59", 0);
|
||||
const tomorrow = today().plus({ days: 1 });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(tomorrow),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "planned",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity span.text-success", { text: "Tomorrow:" });
|
||||
mockDate("2023-12-08 00:00:01");
|
||||
await advanceTime(2000);
|
||||
await contains(".o-mail-Activity span.text-warning", { text: "Today:" });
|
||||
});
|
||||
|
||||
test("activity with a summary layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
summary: "test summary",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity", { text: "“test summary”" });
|
||||
});
|
||||
|
||||
test("activity without summary layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_type_id: 1,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity", { text: "Email" });
|
||||
});
|
||||
|
||||
test("activity details toggle", async () => {
|
||||
mockDate("2023-01-11 12:00:00");
|
||||
const tomorrow = today().plus({ days: 1 });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
pyEnv["mail.activity"].create({
|
||||
create_uid: userId,
|
||||
date_deadline: serializeDate(tomorrow),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-Activity-details", { count: 0 });
|
||||
await contains(".o-mail-Activity i[aria-label='Info']");
|
||||
await click(".o-mail-Activity i[aria-label='Info']");
|
||||
await contains(".o-mail-Activity-details");
|
||||
await click(".o-mail-Activity i[aria-label='Info']");
|
||||
await contains(".o-mail-Activity-details", { count: 0 });
|
||||
});
|
||||
|
||||
test("activity with mail template layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const mailTemplateId = pyEnv["mail.template"].create({ name: "Dummy mail template" });
|
||||
const activityType = pyEnv["mail.activity.type"].find((r) => r.name === "Email");
|
||||
pyEnv["mail.activity.type"].write(activityType.id, { mail_template_ids: [mailTemplateId] });
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_type_id: activityType.id,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-Activity-sidebar");
|
||||
await contains(".o-mail-Activity-mailTemplates");
|
||||
await contains(".o-mail-ActivityMailTemplate-name", { text: "Dummy mail template" });
|
||||
await contains(".o-mail-ActivityMailTemplate-preview");
|
||||
await contains(".o-mail-ActivityMailTemplate-send");
|
||||
});
|
||||
|
||||
test("activity with mail template: preview mail", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const mailTemplateId = pyEnv["mail.template"].create({ name: "Dummy mail template" });
|
||||
const activityType = pyEnv["mail.activity.type"].find((r) => r.name === "Email");
|
||||
pyEnv["mail.activity.type"].write(activityType.id, { mail_template_ids: [mailTemplateId] });
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_type_id: activityType.id,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
if (action?.res_model !== "res.partner") {
|
||||
// Click on Preview Mail Template
|
||||
asyncStep("do_action");
|
||||
expect(action.context.default_res_ids).toEqual([partnerId]);
|
||||
expect(action.context.default_model).toBe("res.partner");
|
||||
expect(action.context.default_template_id).toBe(mailTemplateId);
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.res_model).toBe("mail.compose.message");
|
||||
}
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-ActivityMailTemplate-preview");
|
||||
await click(".o-mail-ActivityMailTemplate-preview");
|
||||
await waitForSteps(["do_action"]);
|
||||
});
|
||||
|
||||
test("activity with mail template: send mail", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const mailTemplateId = pyEnv["mail.template"].create({ name: "Dummy mail template" });
|
||||
const activityType = pyEnv["mail.activity.type"].find((r) => r.name === "Email");
|
||||
pyEnv["mail.activity.type"].write(activityType.id, { mail_template_ids: [mailTemplateId] });
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_type_id: activityType.id,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpc("res.partner", "activity_send_mail", ({ args, method }) => {
|
||||
asyncStep(method);
|
||||
expect(args[0]).toHaveLength(1);
|
||||
expect(args[0][0]).toBe(partnerId);
|
||||
expect(args[1]).toBe(mailTemplateId);
|
||||
// random value returned in order for the mock server to know that this route is implemented.
|
||||
return true;
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-ActivityMailTemplate-send");
|
||||
await click(".o-mail-ActivityMailTemplate-send");
|
||||
await waitForSteps(["activity_send_mail"]);
|
||||
});
|
||||
|
||||
test("activity click on mark as done", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityTypeId = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0];
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "default",
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await contains(".o-mail-ActivityMarkAsDone");
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await contains(".o-mail-ActivityMarkAsDone", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("activity mark as done popover should focus feedback input on open", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityTypeId = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0];
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "default",
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await contains(".o-mail-ActivityMarkAsDone textarea[placeholder='Write Feedback']:focus");
|
||||
});
|
||||
|
||||
test("activity click on edit", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const mailTemplateId = pyEnv["mail.template"].create({ name: "Dummy mail template" });
|
||||
const activityTypeId = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0];
|
||||
const activityId = pyEnv["mail.activity"].create({
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
mail_template_ids: [mailTemplateId],
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
if (action?.res_model !== "res.partner") {
|
||||
asyncStep("do_action");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.res_model).toBe("mail.activity");
|
||||
expect(action.res_id).toBe(activityId);
|
||||
}
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".o-mail-Activity .btn", { text: "Edit" });
|
||||
await waitForSteps(["do_action"]);
|
||||
});
|
||||
|
||||
test("activity click on edit should pass correct context", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const mailTemplateId = pyEnv["mail.template"].create({ name: "Dummy mail template" });
|
||||
const [activityTypeId] = pyEnv["mail.activity.type"].search([["name", "=", "Email"]]);
|
||||
const activityId = pyEnv["mail.activity"].create({
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
mail_template_ids: [mailTemplateId],
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
mockService("action", {
|
||||
async doAction(action) {
|
||||
asyncStep("do_action");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.res_model).toBe("mail.activity");
|
||||
expect(action.res_id).toBe(activityId);
|
||||
expect(action.context).toEqual({
|
||||
default_res_model: "res.partner",
|
||||
default_res_id: partnerId,
|
||||
dialog_size: "large",
|
||||
});
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
await click(".o-mail-Activity .btn", { text: "Edit" });
|
||||
await waitForSteps(["do_action"]);
|
||||
});
|
||||
|
||||
test("activity click on cancel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityTypeId = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0];
|
||||
const activityId = pyEnv["mail.activity"].create({
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpc("mail.activity", "unlink", ({ args, method }) => {
|
||||
asyncStep(method);
|
||||
expect(args[0]).toHaveLength(1);
|
||||
expect(args[0][0]).toBe(activityId);
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".o-mail-Activity .btn", { text: "Cancel" });
|
||||
await contains(".o-mail-Activity", { count: 0 });
|
||||
await waitForSteps(["unlink"]);
|
||||
});
|
||||
|
||||
test("activity mark done popover close on ESCAPE", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityTypeId = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0];
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "default",
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await contains(".o-mail-ActivityMarkAsDone");
|
||||
triggerHotkey("Escape");
|
||||
await contains(".o-mail-ActivityMarkAsDone", { count: 0 });
|
||||
});
|
||||
|
||||
test("activity mark done popover click on discard", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityTypeId = pyEnv["mail.activity.type"].search([["name", "=", "Email"]])[0];
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "default",
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await click(".o-mail-ActivityMarkAsDone button", { text: "Discard" });
|
||||
await contains(".o-mail-ActivityMarkAsDone", { count: 0 });
|
||||
});
|
||||
|
||||
test("Activity are sorted by deadline", async () => {
|
||||
mockDate("2023-01-11 12:00:00");
|
||||
const dateBefore = today().plus({ days: -5 });
|
||||
const dateAfter = today().plus({ days: 4 });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(dateAfter),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "planned",
|
||||
});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(today()),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "today",
|
||||
});
|
||||
pyEnv["mail.activity"].create({
|
||||
date_deadline: serializeDate(dateBefore),
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
state: "overdue",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(":nth-child(1 of .o-mail-Activity)", { text: "5 days overdue:" });
|
||||
await contains(":nth-child(2 of .o-mail-Activity)", { text: "Today:" });
|
||||
await contains(":nth-child(3 of .o-mail-Activity)", { text: "Due in 4 days:" });
|
||||
});
|
||||
|
||||
test("chatter 'activity' button open the activity schedule wizard", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.partner"].create({});
|
||||
mockService("action", {
|
||||
async doAction(action, options) {
|
||||
if (action?.res_model !== "res.partner") {
|
||||
asyncStep("doAction");
|
||||
const expectedAction = {
|
||||
context: {
|
||||
active_ids: [fakeId],
|
||||
active_id: fakeId,
|
||||
active_model: "res.partner",
|
||||
},
|
||||
name: "Schedule Activity",
|
||||
res_model: "mail.activity.schedule",
|
||||
target: "new",
|
||||
type: "ir.actions.act_window",
|
||||
view_mode: "form",
|
||||
views: [[false, "form"]],
|
||||
};
|
||||
expect(action).toEqual(expectedAction, {
|
||||
message: "should execute an action with correct params",
|
||||
});
|
||||
options.onClose();
|
||||
}
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", fakeId, {
|
||||
arch: `
|
||||
<form string="Fake">
|
||||
<sheet></sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await click("button", { text: "Activity" });
|
||||
await waitForSteps(["doAction"]);
|
||||
});
|
||||
|
||||
test("Activity avatar should have a unique timestamp", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
const partner = pyEnv["res.partner"].search_read([["id", "=", serverState.partnerId]])[0];
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(
|
||||
`.o-mail-Activity-sidebar img[data-src="${getOrigin()}/web/image/res.partner/${
|
||||
serverState.partnerId
|
||||
}/avatar_128?unique=${deserializeDateTime(partner.write_date).ts}`
|
||||
);
|
||||
});
|
||||
|
||||
test("activity with a link to a record", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId1 = pyEnv["res.partner"].create({ name: "Partner 1" });
|
||||
const partnerId2 = pyEnv["res.partner"].create({ name: "Partner 2" });
|
||||
pyEnv["mail.activity"].create({
|
||||
note: `<p>Activity with a link to a <a href="#" data-oe-model="res.partner" data-oe-id="${partnerId2}">record</a></p>`,
|
||||
res_id: partnerId1,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId1);
|
||||
await click(".o-mail-Activity-note a", { text: "record" });
|
||||
await contains(".o_form_view input", { value: "Partner 2" });
|
||||
});
|
||||
|
||||
test("activity with a user mention", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId1 = pyEnv["res.partner"].create({ name: "Partner 1" });
|
||||
const partnerId2 = pyEnv["res.partner"].create({ name: "Partner 2" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId2 });
|
||||
pyEnv["mail.activity"].create({
|
||||
note: `<p>How are you, <a class="o_mail_redirect" href="#" data-oe-model="res.partner" data-oe-id="${partnerId2}">@Partner 2</a>?</p>`,
|
||||
res_id: partnerId1,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId1);
|
||||
await click(".o-mail-Activity-note a", { text: "@Partner 2" });
|
||||
await contains(".o_avatar_card:contains('Partner 2')");
|
||||
});
|
||||
|
||||
test("activity with a channel mention", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Partner" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Channel", channel_type: "channel" });
|
||||
pyEnv["mail.activity"].create({
|
||||
note: `<p><a class="o_channel_redirect" href="#" data-oe-model="discuss.channel" data-oe-id="${channelId}">#Channel</a></p>`,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".o-mail-Activity-note a", { text: "#Channel" });
|
||||
await contains(".o-mail-ChatWindow-header", { text: "Channel" });
|
||||
});
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
import { asyncStep, mockService, onRpc, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("activity mark done popover simplest layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "not_upload_file",
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await contains(".o-mail-ActivityMarkAsDone");
|
||||
await contains(".o-mail-ActivityMarkAsDone textarea[placeholder='Write Feedback']");
|
||||
await contains(".o-mail-ActivityMarkAsDone button[aria-label='Done and Schedule Next']");
|
||||
await contains(".o-mail-ActivityMarkAsDone button[aria-label='Done']");
|
||||
await contains(".o-mail-ActivityMarkAsDone button", { text: "Discard" });
|
||||
});
|
||||
|
||||
test("activity with force next mark done popover simplest layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityTypeId = pyEnv["mail.activity.type"].create({
|
||||
name: "TriggerType",
|
||||
chaining_type: "trigger",
|
||||
});
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "not_upload_file",
|
||||
activity_type_id: activityTypeId,
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await contains(".o-mail-ActivityMarkAsDone");
|
||||
await contains(".o-mail-ActivityMarkAsDone textarea[placeholder='Write Feedback']");
|
||||
await contains(".o-mail-ActivityMarkAsDone button[aria-label='Done and Schedule Next']");
|
||||
await contains(".o-mail-ActivityMarkAsDone button[aria-label='Done']", { count: 0 });
|
||||
await contains(".o-mail-ActivityMarkAsDone button", { text: "Discard" });
|
||||
});
|
||||
|
||||
test("activity mark done popover mark done without feedback", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityId = pyEnv["mail.activity"].create({
|
||||
activity_category: "not_upload_file",
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpc("mail.activity", "action_feedback", ({ args, kwargs }) => {
|
||||
asyncStep("action_feedback");
|
||||
expect(args).toHaveLength(1);
|
||||
expect(args[0]).toHaveLength(1);
|
||||
expect(args[0][0]).toBe(activityId);
|
||||
expect(kwargs.attachment_ids).toBeEmpty();
|
||||
expect(kwargs).not.toInclude("feedback");
|
||||
// random value returned in order for the mock server to know that this route is implemented.
|
||||
return true;
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await click(".o-mail-ActivityMarkAsDone button[aria-label='Done']");
|
||||
await waitForSteps(["action_feedback"]);
|
||||
});
|
||||
|
||||
test("activity mark done popover mark done with feedback", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityId = pyEnv["mail.activity"].create({
|
||||
activity_category: "not_upload_file",
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpc("mail.activity", "action_feedback", ({ args, kwargs, method }) => {
|
||||
asyncStep(method);
|
||||
expect(args).toHaveLength(1);
|
||||
expect(args[0]).toHaveLength(1);
|
||||
expect(args[0][0]).toBe(activityId);
|
||||
expect(kwargs.attachment_ids).toBeEmpty();
|
||||
expect(kwargs.feedback).toBe("This task is done");
|
||||
// random value returned in order for the mock server to know that this route is implemented.
|
||||
return true;
|
||||
});
|
||||
onRpc("mail.activity", "unlink", () => {
|
||||
// 'unlink' on non-existing record raises a server crash
|
||||
throw new Error(
|
||||
"'unlink' RPC on activity must not be called (already unlinked from mark as done)"
|
||||
);
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await insertText(
|
||||
".o-mail-ActivityMarkAsDone textarea[placeholder='Write Feedback']",
|
||||
"This task is done"
|
||||
);
|
||||
await click(".o-mail-ActivityMarkAsDone button[aria-label='Done']");
|
||||
await waitForSteps(["action_feedback"]);
|
||||
});
|
||||
|
||||
test("activity mark done popover mark done and schedule next", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const activityId = pyEnv["mail.activity"].create({
|
||||
activity_category: "not_upload_file",
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpc("mail.activity", "action_feedback_schedule_next", ({ args, kwargs, method }) => {
|
||||
asyncStep(method);
|
||||
expect(args).toHaveLength(1);
|
||||
expect(args[0]).toHaveLength(1);
|
||||
expect(args[0][0]).toBe(activityId);
|
||||
expect(kwargs.feedback).toBe("This task is done");
|
||||
return false;
|
||||
});
|
||||
onRpc("mail.activity", "unlink", () => {
|
||||
// 'unlink' on non-existing record raises a server crash
|
||||
throw new Error(
|
||||
"'unlink' RPC on activity must not be called (already unlinked from mark as done)"
|
||||
);
|
||||
});
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
if (action?.res_model !== "res.partner") {
|
||||
asyncStep("activity_action");
|
||||
throw new Error(
|
||||
"The do-action event should not be triggered when the route doesn't return an action"
|
||||
);
|
||||
}
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await insertText(
|
||||
".o-mail-ActivityMarkAsDone textarea[placeholder='Write Feedback']",
|
||||
"This task is done"
|
||||
);
|
||||
await click(".o-mail-ActivityMarkAsDone button[aria-label='Done and Schedule Next']");
|
||||
await waitForSteps(["action_feedback_schedule_next"]);
|
||||
});
|
||||
|
||||
test("[technical] activity mark done & schedule next with new action", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
activity_category: "not_upload_file",
|
||||
can_write: true,
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpc("mail.activity", "action_feedback_schedule_next", () => ({
|
||||
type: "ir.actions.act_window",
|
||||
}));
|
||||
const def = new Deferred();
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
if (action?.res_model !== "res.partner") {
|
||||
def.resolve();
|
||||
asyncStep("activity_action");
|
||||
expect(action).toEqual(
|
||||
{ type: "ir.actions.act_window" },
|
||||
{ message: "The content of the action should be correct" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".btn", { text: "Mark Done" });
|
||||
await click(".o-mail-ActivityMarkAsDone button[aria-label='Done and Schedule Next']");
|
||||
await def;
|
||||
await waitForSteps(["activity_action"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { ActivityMenu } from "@mail/core/web/activity_menu";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, queryText } from "@odoo/hoot-dom";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("should update activities when opening the activity menu", async () => {
|
||||
const pyEnv = await startServer();
|
||||
await start();
|
||||
await contains(".o_menu_systray i[aria-label='Activities']");
|
||||
await contains(".o-mail-ActivityMenu-counter", { count: 0 });
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.activity"].create({
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await click(".o_menu_systray i[aria-label='Activities']");
|
||||
await contains(".o-mail-ActivityMenu-counter", { text: "1" });
|
||||
});
|
||||
|
||||
test("global shortcut", async () => {
|
||||
await mountWithCleanup(ActivityMenu);
|
||||
await triggerHotkey("control+k");
|
||||
await animationFrame();
|
||||
expect(queryText(`.o_command:contains("Activity") .o_command_hotkey`)).toEqual(
|
||||
"Activity\nALT + SHIFT + A",
|
||||
{ message: "The command should be registered with the right hotkey" }
|
||||
);
|
||||
await triggerHotkey("alt+shift+a");
|
||||
await animationFrame();
|
||||
expect(".modal-dialog .modal-title").toHaveText("Schedule Activity");
|
||||
});
|
||||
|
|
@ -0,0 +1,584 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { leave, runAllTimers } from "@odoo/hoot-dom";
|
||||
import { Command, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
import {
|
||||
assertChatHub,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
hover,
|
||||
insertText,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
openFormView,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
triggerEvents,
|
||||
triggerHotkey,
|
||||
} from "../mail_test_helpers";
|
||||
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { range } from "@web/core/utils/numbers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Folded chat windows are displayed as chat bubbles", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = pyEnv["discuss.channel"].create([
|
||||
{ name: "Channel A" },
|
||||
{ name: "Channel B" },
|
||||
]);
|
||||
setupChatHub({ folded: channelIds });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble", { count: 2 });
|
||||
await click(".o-mail-ChatBubble", { count: 2 });
|
||||
await contains(".o-mail-ChatBubble", { count: 1 });
|
||||
await contains(".o-mail-ChatWindow", { count: 1 });
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("No duplicated chat bubbles", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
await start();
|
||||
// Make bubble of "John" chat
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-MessagingMenu button", { text: "New Message" });
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await insertText("input[placeholder='Search a conversation']", "John");
|
||||
await contains(".o_command_name", { count: 3 });
|
||||
await click(".o_command_name", { text: "John" });
|
||||
await contains(".o-mail-ChatWindow", { text: "John" });
|
||||
await contains(".o-mail-ChatWindow", {
|
||||
text: "This is the start of your direct chat with John",
|
||||
}); // wait fully loaded
|
||||
await click("button[title='Fold']");
|
||||
await contains(".o-mail-ChatBubble[name='John']");
|
||||
// Make bubble of "John" chat again
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-MessagingMenu button", { text: "New Message" });
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await insertText("input[placeholder='Search a conversation']", "John");
|
||||
await contains(".o_command_name", { count: 3 });
|
||||
await click(".o_command_name", { text: "John" });
|
||||
await contains(".o-mail-ChatBubble[name='John']", { count: 0 });
|
||||
await contains(".o-mail-ChatWindow", { text: "John" });
|
||||
await click(".o-mail-ChatWindow-header [title='Fold']");
|
||||
// Make again from click messaging menu item
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem");
|
||||
await contains(".o-mail-ChatBubble[name='John']", { count: 0 });
|
||||
await contains(".o-mail-ChatWindow", { text: "John" });
|
||||
});
|
||||
|
||||
test("Up to 7 chat bubbles", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = [];
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
channelIds.push(pyEnv["discuss.channel"].create({ name: String(i) }));
|
||||
}
|
||||
setupChatHub({ folded: channelIds.reverse() });
|
||||
await start();
|
||||
for (let i = 8; i > 1; i--) {
|
||||
await contains(`.o-mail-ChatBubble[name='${String(i)}']`);
|
||||
}
|
||||
await contains(".o-mail-ChatBubble[name='1']", { count: 0 });
|
||||
await contains(".o-mail-ChatHub-hiddenBtn", { text: "+1" });
|
||||
await hover(".o-mail-ChatHub-hiddenBtn");
|
||||
await contains(".o-mail-ChatHub-hiddenItem[name='1']");
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
await click(".o-mail-ChatHub-hiddenItem");
|
||||
await contains(".o-mail-ChatWindow", { count: 1 });
|
||||
await contains(".o-mail-ChatHub-hiddenBtn", { count: 0 });
|
||||
});
|
||||
|
||||
test("Ordering of chat bubbles is consistent and seems logical.", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
const channelIds = [channelId];
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
channelIds.push(pyEnv["discuss.channel"].create({ name: String(i) }));
|
||||
}
|
||||
setupChatHub({ folded: channelIds.reverse() });
|
||||
await start();
|
||||
// FIXME: expect arbitrary order 7, 6, 5, 4, 3, 2, 1
|
||||
await contains(":nth-child(1 of .o-mail-ChatBubble)[name='7']");
|
||||
await contains(":nth-child(2 of .o-mail-ChatBubble)[name='6']");
|
||||
await contains(":nth-child(3 of .o-mail-ChatBubble)[name='5']");
|
||||
await contains(":nth-child(4 of .o-mail-ChatBubble)[name='4']");
|
||||
await contains(":nth-child(5 of .o-mail-ChatBubble)[name='3']");
|
||||
await contains(":nth-child(6 of .o-mail-ChatBubble)[name='2']");
|
||||
await contains(":nth-child(7 of .o-mail-ChatBubble)[name='1']");
|
||||
await contains(".o-mail-ChatBubble[name='Demo']", { count: 0 });
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
await click(".o-mail-ChatBubble[name='3']");
|
||||
await contains(".o-mail-ChatWindow", { text: "3" });
|
||||
await contains(":nth-child(7 of .o-mail-ChatBubble)[name='Demo']");
|
||||
await click(".o-mail-ChatWindow-header [title='Fold']");
|
||||
await contains(".o-mail-ChatBubble[name='Demo']", { count: 0 });
|
||||
await click(".o-mail-ChatBubble[name='4']");
|
||||
await contains(":nth-child(1 of .o-mail-ChatBubble)[name='3']");
|
||||
await contains(":nth-child(2 of .o-mail-ChatBubble)[name='7']");
|
||||
await contains(":nth-child(3 of .o-mail-ChatBubble)[name='6']");
|
||||
await contains(":nth-child(7 of .o-mail-ChatBubble)[name='Demo']");
|
||||
await click(".o-mail-ChatWindow-header [title='Fold']");
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
// no reorder on receiving new message
|
||||
withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "test", message_type: "comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await hover(".o-mail-ChatHub-hiddenBtn");
|
||||
await contains(".o-mail-ChatHub-hiddenItem[name='Demo']");
|
||||
});
|
||||
|
||||
test("Hover on chat bubble shows chat name + last message preview", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const marcPartnerId = pyEnv["res.partner"].create({ name: "Marc" });
|
||||
const marcChannelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: marcPartnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello!",
|
||||
model: "discuss.channel",
|
||||
author_id: marcPartnerId,
|
||||
res_id: marcChannelId,
|
||||
});
|
||||
const demoPartnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const demoChannelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: demoPartnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
setupChatHub({ folded: [marcChannelId, demoChannelId] });
|
||||
await start();
|
||||
await hover(".o-mail-ChatBubble[name='Marc']");
|
||||
await contains(".o-mail-ChatBubble-preview", { text: "MarcHello!" });
|
||||
await leave();
|
||||
await contains(".o-mail-ChatBubble-preview", { count: 0 });
|
||||
await hover(".o-mail-ChatBubble[name='Demo']");
|
||||
await contains(".o-mail-ChatBubble-preview", { text: "Demo" });
|
||||
await leave();
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "Hi", message_type: "comment" },
|
||||
thread_id: demoChannelId,
|
||||
thread_model: "discuss.channel",
|
||||
});
|
||||
await hover(".o-mail-ChatBubble[name='Demo']");
|
||||
await contains(".o-mail-ChatBubble-preview", { text: "DemoYou: Hi" });
|
||||
});
|
||||
|
||||
test("Chat bubble preview works on author as email address", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["discuss.channel"].create({ name: "test channel" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: null,
|
||||
body: "Some email message",
|
||||
email_from: "md@oilcompany.fr",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem");
|
||||
await click(".o-mail-ChatWindow [title='Fold']");
|
||||
await hover(".o-mail-ChatBubble");
|
||||
await contains(".o-mail-ChatBubble-preview", { text: "md@oilcompany.fr: Some email message" });
|
||||
});
|
||||
|
||||
test("chat bubbles are synced between tabs", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const marcPartnerId = pyEnv["res.partner"].create({ name: "Marc" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: marcPartnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
setupChatHub({ folded: [channelId] });
|
||||
const tab1 = await start({ asTab: true });
|
||||
const tab2 = await start({ asTab: true });
|
||||
await contains(`${tab1.selector} .o-mail-ChatBubble`);
|
||||
await contains(`${tab2.selector} .o-mail-ChatBubble`);
|
||||
await runAllTimers(); // Wait for bus service to fully load
|
||||
await click(`${tab1.selector} .o-mail-ChatBubble[name='Marc']`);
|
||||
await contains(`${tab2.selector} .o-mail-ChatWindow`); // open sync
|
||||
await click(`${tab2.selector} .o-mail-ChatWindow-header [title='Fold']`);
|
||||
await contains(`${tab1.selector} .o-mail-ChatWindow`, { count: 0 }); // fold sync
|
||||
await click(`${tab1.selector} .o-mail-ChatBubble[name='Marc'] .o-mail-ChatBubble-close`);
|
||||
await contains(`${tab2.selector} .o-mail-ChatBubble[name='Marc']`, { count: 0 }); // close sync
|
||||
});
|
||||
|
||||
test("Chat bubbles do not fetch messages until becoming open", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [channeId1, channelId2] = pyEnv["discuss.channel"].create([
|
||||
{ name: "Orange" },
|
||||
{ name: "Apple" },
|
||||
]);
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "Orange",
|
||||
res_id: channeId1,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
body: "Apple",
|
||||
res_id: channelId2,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
]);
|
||||
onRpcBefore("/discuss/channel/messages", () => expect.step("fetch_messages"));
|
||||
setupChatHub({ folded: [channeId1, channelId2] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble[name='Orange']");
|
||||
expect.verifySteps([]);
|
||||
await click(".o-mail-ChatBubble[name='Orange']");
|
||||
await contains(".o-mail-ChatWindow");
|
||||
await contains(".o-mail-Message-content", { text: "Orange" });
|
||||
await contains(".o-mail-Message-content", { count: 0, text: "Apple" });
|
||||
expect.verifySteps(["fetch_messages"]); // from "Orange" becoming open
|
||||
});
|
||||
|
||||
test("More than 7 actually folded chat windows shows a 'hidden' chat bubble menu", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = [];
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
channelIds.push(pyEnv["discuss.channel"].create({ name: String(i) }));
|
||||
}
|
||||
setupChatHub({ folded: channelIds.reverse() });
|
||||
await start();
|
||||
// Can make chat from hidden menu
|
||||
await hover(".o-mail-ChatHub-hiddenBtn");
|
||||
await click(".o-mail-ChatHub-hiddenItem");
|
||||
await leave(); // FIXME: hover is persistent otherwise
|
||||
await contains(".o-mail-ChatHub-hiddenItem", { count: 0 });
|
||||
await contains(".o-mail-ChatHub-hiddenBtn", { count: 0 });
|
||||
await contains(".o-mail-ChatWindow");
|
||||
await click(".o-mail-ChatWindow-header [title='Fold']");
|
||||
// Can open hidden chat from messaging menu
|
||||
await click("i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem", { text: "2" });
|
||||
await contains(".o-mail-ChatHub-hiddenItem", { count: 0 });
|
||||
await contains(".o-mail-ChatHub-hiddenBtn", { count: 0 });
|
||||
await contains(".o-mail-ChatWindow");
|
||||
await click(".o-mail-ChatWindow-header [title='Fold']");
|
||||
// Can close chat from hidden menu.
|
||||
await hover(".o-mail-ChatHub-hiddenBtn");
|
||||
await hover(".o-mail-ChatHub-hiddenItem");
|
||||
await click(".o-mail-ChatHub-hiddenClose");
|
||||
await contains(".o-mail-ChatHub-hiddenItem", { count: 0 });
|
||||
await contains(".o-mail-ChatHub-hiddenBtn", { count: 0 });
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
});
|
||||
|
||||
test("Can close all chat windows at once", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = pyEnv["discuss.channel"].create(
|
||||
Array(20)
|
||||
.keys()
|
||||
.map((i) => ({ name: String(i) }))
|
||||
);
|
||||
setupChatHub({ folded: channelIds.reverse() });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble", { count: 8 }); // max reached
|
||||
await contains(".o-mail-ChatBubble", { text: "+13" });
|
||||
await hover(".o-mail-ChatHub-hiddenBtn");
|
||||
await click("button[title='Chat Options']");
|
||||
await click(".o-dropdown-item", { text: "Close all conversations" });
|
||||
await contains(".o-mail-ChatBubble", { count: 0 });
|
||||
assertChatHub({});
|
||||
});
|
||||
|
||||
test("Don't show chat hub in discuss app", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = pyEnv["discuss.channel"].create(
|
||||
range(0, 20).map((i) => ({ name: String(i) }))
|
||||
);
|
||||
setupChatHub({ folded: channelIds.reverse() });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble", { count: 8 }); // max reached
|
||||
await contains(".o-mail-ChatBubble", { text: "+13" });
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-ChatBubble", { count: 0 });
|
||||
});
|
||||
|
||||
test("Can compact chat hub", async () => {
|
||||
// allows to temporarily reduce footprint of chat windows on UI
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
channelIds.push(pyEnv["discuss.channel"].create({ name: String(i) }));
|
||||
}
|
||||
setupChatHub({ folded: channelIds.reverse() });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble", { count: 8 }); // max reached
|
||||
await contains(".o-mail-ChatBubble", { text: "+13" });
|
||||
await hover(".o-mail-ChatHub-hiddenBtn");
|
||||
await click("button[title='Chat Options']");
|
||||
await click(".o-dropdown-item", { text: "Hide all conversations" });
|
||||
await contains(".o-mail-ChatBubble i.fa.fa-comments");
|
||||
await click(".o-mail-ChatBubble i.fa.fa-comments");
|
||||
await contains(".o-mail-ChatBubble", { count: 8 });
|
||||
// alternative compact: click hidden button
|
||||
await click(".o-mail-ChatBubble", { text: "+13" });
|
||||
await contains(".o-mail-ChatBubble i.fa.fa-comments");
|
||||
// don't show compact button in discuss app
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-Discuss[data-active]");
|
||||
await contains(".o-mail-ChatBubble i.fa.fa-comments", { count: 0 });
|
||||
});
|
||||
|
||||
test("Compact chat hub is crosstab synced", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = pyEnv["discuss.channel"].create([{ name: "ch-1" }, { name: "ch-2" }]);
|
||||
setupChatHub({ folded: channelIds });
|
||||
const env1 = await start({ asTab: true });
|
||||
const env2 = await start({ asTab: true });
|
||||
await contains(`${env1.selector} .o-mail-ChatBubble`, { count: 2 });
|
||||
await contains(`${env2.selector} .o-mail-ChatBubble`, { count: 2 });
|
||||
await hover(`${env1.selector} .o-mail-ChatBubble:eq(0)`);
|
||||
await click(`${env1.selector} button[title='Chat Options']`);
|
||||
await click(`${env1.selector} .o-dropdown-item`, { text: "Hide all conversations" });
|
||||
await contains(`${env1.selector} .o-mail-ChatBubble .fa-comments`);
|
||||
await contains(`${env2.selector} .o-mail-ChatBubble .fa-comments`);
|
||||
});
|
||||
|
||||
test("Compacted chat hub shows badge with amount of hidden chats with important messages", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelIds = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const partner_id = pyEnv["res.partner"].create({ name: `partner_${i}` });
|
||||
const chatId = pyEnv["discuss.channel"].create({
|
||||
name: String(i),
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
channelIds.push(chatId);
|
||||
if (i < 10) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello!",
|
||||
model: "discuss.channel",
|
||||
author_id: partner_id,
|
||||
res_id: chatId,
|
||||
});
|
||||
}
|
||||
}
|
||||
setupChatHub({ folded: channelIds });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble", { count: 8 }); // max reached
|
||||
await contains(".o-mail-ChatBubble", { text: "+13" });
|
||||
await click(".o-mail-ChatHub-hiddenBtn");
|
||||
await contains(".o-mail-ChatBubble i.fa.fa-comments");
|
||||
await contains(".o-mail-ChatBubble .o-discuss-badge", { text: "9" });
|
||||
});
|
||||
|
||||
test("Show IM status", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const demoId = pyEnv["res.partner"].create({ name: "Demo User", im_status: "online" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: demoId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
setupChatHub({ folded: [channelId] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble .fa-circle.text-success[aria-label='User is online']");
|
||||
});
|
||||
|
||||
test("Attachment-only message preview shows file name", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partner1, partner2, partner3] = pyEnv["res.partner"].create([
|
||||
{ name: "Partner1" },
|
||||
{ name: "Partner2" },
|
||||
{ name: "Partner3" },
|
||||
]);
|
||||
const [channel1, channel2, channel3] = pyEnv["discuss.channel"].create([
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partner1 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partner2 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partner3 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
attachment_ids: [
|
||||
Command.create({
|
||||
mimetype: "application/pdf",
|
||||
name: "File.pdf",
|
||||
res_id: channel1,
|
||||
res_model: "discuss.channel",
|
||||
}),
|
||||
],
|
||||
author_id: partner1,
|
||||
body: "",
|
||||
model: "discuss.channel",
|
||||
res_id: channel1,
|
||||
},
|
||||
{
|
||||
attachment_ids: [
|
||||
Command.create({
|
||||
mimetype: "image/jpeg",
|
||||
name: "Image.jpeg",
|
||||
res_id: channel2,
|
||||
res_model: "discuss.channel",
|
||||
}),
|
||||
Command.create({
|
||||
mimetype: "application/pdf",
|
||||
name: "File.pdf",
|
||||
res_id: channel2,
|
||||
res_model: "discuss.channel",
|
||||
}),
|
||||
],
|
||||
author_id: partner2,
|
||||
body: "",
|
||||
model: "discuss.channel",
|
||||
res_id: channel2,
|
||||
},
|
||||
{
|
||||
attachment_ids: [
|
||||
Command.create({
|
||||
mimetype: "application/pdf",
|
||||
name: "File.pdf",
|
||||
res_id: channel3,
|
||||
res_model: "discuss.channel",
|
||||
}),
|
||||
Command.create({
|
||||
mimetype: "image/jpeg",
|
||||
name: "Image.jpeg",
|
||||
res_id: channel3,
|
||||
res_model: "discuss.channel",
|
||||
}),
|
||||
Command.create({
|
||||
mimetype: "video/mp4",
|
||||
name: "Video.mp4",
|
||||
res_id: channel3,
|
||||
res_model: "discuss.channel",
|
||||
}),
|
||||
],
|
||||
author_id: partner3,
|
||||
body: "",
|
||||
model: "discuss.channel",
|
||||
res_id: channel3,
|
||||
},
|
||||
]);
|
||||
setupChatHub({ folded: [channel1, channel2, channel3] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble[name='Partner1']");
|
||||
await hover(".o-mail-ChatBubble[name='Partner1']");
|
||||
await contains(".o-mail-ChatBubble-preview", { text: "Partner1File.pdf" });
|
||||
await contains(".o-mail-ChatBubble[name='Partner2']");
|
||||
await hover(".o-mail-ChatBubble[name='Partner2']");
|
||||
await contains(".o-mail-ChatBubble-preview", { text: "Partner2Image.jpeg and File.pdf" });
|
||||
await contains(".o-mail-ChatBubble[name='Partner3']");
|
||||
await hover(".o-mail-ChatBubble[name='Partner3']");
|
||||
await contains(".o-mail-ChatBubble-preview", {
|
||||
text: "Partner3File.pdf and 2 other attachments",
|
||||
});
|
||||
});
|
||||
|
||||
test("Open chat window from messaging menu with chat hub compact", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const johnId = pyEnv["res.users"].create({ name: "John" });
|
||||
const johnPartnerId = pyEnv["res.partner"].create({ user_ids: [johnId], name: "John" });
|
||||
const chatId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: johnPartnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
setupChatHub({ folded: [chatId] });
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click("button[title='Chat Options']");
|
||||
await click(".o-dropdown-item", { text: "Hide all conversations" });
|
||||
await contains(".o-mail-ChatHub-compact");
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem", { text: "John" });
|
||||
await contains(".o-mail-ChatWindow", { text: "John" });
|
||||
await triggerEvents(".o-mail-Composer-input", ["blur", "focusout"]); // FIXME: click fold doesn't focusout/blur the composer, thus marks as read
|
||||
await click(".o-mail-ChatWindow-header [title='Fold']");
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
await withUser(johnId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "Hello Mitchel!", message_type: "comment" },
|
||||
thread_id: chatId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-ChatHub-compact", { text: "1" });
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
});
|
||||
|
||||
test("Open chat window from command palette with chat hub compact", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const johnId = pyEnv["res.users"].create({ name: "John" });
|
||||
const johnPartnerId = pyEnv["res.partner"].create({ user_ids: [johnId], name: "John" });
|
||||
const chatId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: johnPartnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
setupChatHub({ folded: [chatId] });
|
||||
await start();
|
||||
await click("button[title='Chat Options']");
|
||||
await click(".o-dropdown-item", { text: "Hide all conversations" });
|
||||
await contains(".o-mail-ChatHub-compact");
|
||||
await triggerHotkey("control+k");
|
||||
await insertText(".o_command_palette_search input", "@");
|
||||
await click(".o-mail-DiscussCommand", { text: "John" });
|
||||
await contains(".o-mail-ChatWindow", { text: "John" });
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,125 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
onRpcBefore,
|
||||
patchUiSize,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { CHAT_HUB_KEY } from "@mail/core/common/chat_hub_model";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { asyncStep, getService, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("chat window does not fetch messages if hidden", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [channeId1, channelId2, channelId3] = pyEnv["discuss.channel"].create([{}, {}, {}]);
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "Orange",
|
||||
res_id: channeId1,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
body: "Apple",
|
||||
res_id: channelId2,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
body: "Banana",
|
||||
res_id: channelId3,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
]);
|
||||
patchUiSize({ width: 900 }); // enough for 2 open chat windows max
|
||||
onRpcBefore("/discuss/channel/messages", () => asyncStep("fetch_messages"));
|
||||
setupChatHub({ opened: [channelId3, channelId2, channeId1] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatWindow", { count: 2 });
|
||||
await contains(".o-mail-ChatBubble", { count: 1 });
|
||||
// FIXME: expected ordering: Banana, Apple, Orange
|
||||
await contains(".o-mail-Message-content", { text: "Banana" });
|
||||
await contains(".o-mail-Message-content", { text: "Apple" });
|
||||
await contains(".o-mail-Message-content", { count: 0, text: "Orange" });
|
||||
await waitForSteps(["fetch_messages", "fetch_messages"]);
|
||||
});
|
||||
|
||||
test("click on hidden chat window should fetch its messages", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [channeId1, channelId2, channelId3] = pyEnv["discuss.channel"].create([{}, {}, {}]);
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "Orange",
|
||||
res_id: channeId1,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
body: "Apple",
|
||||
res_id: channelId2,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
body: "Banana",
|
||||
res_id: channelId3,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
]);
|
||||
patchUiSize({ width: 900 }); // enough for 2 open chat windows max
|
||||
onRpcBefore("/discuss/channel/messages", () => asyncStep("fetch_messages"));
|
||||
setupChatHub({ opened: [channelId3, channelId2, channeId1] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatWindow", { count: 2 });
|
||||
await contains(".o-mail-ChatBubble", { count: 1 });
|
||||
// FIXME: expected ordering: Banana, Apple, Orange
|
||||
await contains(".o-mail-Message-content", { text: "Banana" });
|
||||
await contains(".o-mail-Message-content", { text: "Apple" });
|
||||
await contains(".o-mail-Message-content", { count: 0, text: "Orange" });
|
||||
await waitForSteps(["fetch_messages", "fetch_messages"]);
|
||||
await click(".o-mail-ChatBubble");
|
||||
await contains(".o-mail-Message-content", { text: "Orange" });
|
||||
await contains(".o-mail-Message-content", { text: "Banana" });
|
||||
await contains(".o-mail-Message", { count: 0, text: "Apple" });
|
||||
await waitForSteps(["fetch_messages"]);
|
||||
});
|
||||
|
||||
test("downgrade 19.1 to 19.0 should ignore chat hub local storage data", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
// simulate data in local storage like 19.1
|
||||
browser.localStorage.setItem(
|
||||
CHAT_HUB_KEY,
|
||||
JSON.stringify({
|
||||
opened: [{ id: channelId }],
|
||||
folded: [{ id: 1000 }],
|
||||
})
|
||||
);
|
||||
await start();
|
||||
const store = getService("mail.store");
|
||||
await store.chatHub.initPromise;
|
||||
expect(browser.localStorage.getItem(CHAT_HUB_KEY)).toBe(null);
|
||||
await contains(".o-mail-ChatHub");
|
||||
await contains(".o-mail-ChatHub .o-mail-ChatWindow", { count: 0 });
|
||||
await contains(".o-mail-ChatHub .o-mail-ChatBubble", { count: 0 });
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem");
|
||||
await contains(".o-mail-ChatWindow");
|
||||
expect(browser.localStorage.getItem(CHAT_HUB_KEY)).toBe(
|
||||
JSON.stringify({ opened: [{ id: channelId, model: "discuss.channel" }], folded: [] })
|
||||
);
|
||||
await click(".o-mail-ChatWindow-header [title='Fold']");
|
||||
await contains(".o-mail-ChatBubble");
|
||||
expect(browser.localStorage.getItem(CHAT_HUB_KEY)).toBe(
|
||||
JSON.stringify({ opened: [], folded: [{ id: channelId, model: "discuss.channel" }] })
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import {
|
||||
SIZES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("base non-empty rendering", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blu.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form>
|
||||
<sheet></sheet>
|
||||
<chatter open_attachments="True"/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o-mail-AttachmentBox");
|
||||
await contains("button", { text: "Attach files" });
|
||||
await contains(".o-mail-Chatter input[type='file']");
|
||||
await contains(".o-mail-AttachmentList");
|
||||
});
|
||||
|
||||
test("remove attachment should ask for confirmation", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["ir.attachment"].create({
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form>
|
||||
<sheet></sheet>
|
||||
<chatter open_attachments="True"/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o-mail-AttachmentCard");
|
||||
await contains("button[title='Remove']");
|
||||
await click("button[title='Remove']");
|
||||
await contains(".modal-body", { text: 'Do you really want to delete "Blah.txt"?' });
|
||||
// Confirm the deletion
|
||||
await click(".modal-footer .btn-primary");
|
||||
await contains(".o-mail-AttachmentImage", { count: 0 });
|
||||
});
|
||||
|
||||
test("view attachments", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blu.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form>
|
||||
<sheet></sheet>
|
||||
<chatter open_attachments="True"/>
|
||||
</form>`,
|
||||
});
|
||||
await click('.o-mail-AttachmentContainer[aria-label="Blah.txt"] .o-mail-AttachmentCard-image');
|
||||
await contains(".o-FileViewer");
|
||||
await contains(".o-FileViewer-header", { text: "Blah.txt" });
|
||||
await contains(".o-FileViewer div[aria-label='Next']");
|
||||
await click(".o-FileViewer div[aria-label='Next']");
|
||||
await contains(".o-FileViewer-header", { text: "Blu.txt" });
|
||||
await contains(".o-FileViewer div[aria-label='Next']");
|
||||
await click(".o-FileViewer div[aria-label='Next']");
|
||||
await contains(".o-FileViewer-header", { text: "Blah.txt" });
|
||||
});
|
||||
|
||||
test("scroll to attachment box when toggling on", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
for (let i = 0; i < 30; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty".repeat(50),
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
}
|
||||
pyEnv["ir.attachment"].create({
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await scroll(".o-mail-Chatter", "bottom");
|
||||
await click("button[aria-label='Attach files']");
|
||||
await contains(".o-mail-AttachmentBox");
|
||||
await contains(".o-mail-Chatter", { scroll: 0 });
|
||||
await contains(".o-mail-AttachmentBox", { visible: true });
|
||||
});
|
||||
|
||||
test("do not auto-scroll to attachment box when initially open", async () => {
|
||||
patchUiSize({ size: SIZES.LG });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["ir.attachment"].create({
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form>
|
||||
${`<sheet><field name="name"/></sheet>`.repeat(100)}
|
||||
<chatter open_attachments="True"/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o-mail-Message");
|
||||
// weak test, no guarantee that we waited long enough for the potential scroll to happen
|
||||
await contains(".o_content", { scroll: 0 });
|
||||
});
|
||||
|
||||
test("attachment box should order attachments from newest to oldest", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const resData = { res_id: partnerId, res_model: "res.partner" };
|
||||
pyEnv["ir.attachment"].create([
|
||||
{ name: "A.txt", mimetype: "text/plain", ...resData },
|
||||
{ name: "B.txt", mimetype: "text/plain", ...resData },
|
||||
{ name: "C.txt", mimetype: "text/plain", ...resData },
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter [aria-label='Attach files']", { text: "3" });
|
||||
await click(".o-mail-Chatter [aria-label='Attach files']"); // open attachment box
|
||||
await contains(":nth-child(1 of .o-mail-AttachmentContainer)", { text: "C.txt" });
|
||||
await contains(":nth-child(2 of .o-mail-AttachmentContainer)", { text: "B.txt" });
|
||||
await contains(":nth-child(3 of .o-mail-AttachmentContainer)", { text: "A.txt" });
|
||||
});
|
||||
|
||||
test("attachment box auto-closed on switch to record wih no attachments", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ display_name: "first partner" },
|
||||
{ display_name: "second partner" },
|
||||
]);
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1, {
|
||||
arch: `
|
||||
<form>
|
||||
<sheet></sheet>
|
||||
<chatter open_attachments="True"/>
|
||||
</form>`,
|
||||
resIds: [partnerId_1, partnerId_2],
|
||||
});
|
||||
await contains(".o-mail-AttachmentBox");
|
||||
await click(".o_pager_next");
|
||||
await contains(".o-mail-AttachmentBox", { count: 0 });
|
||||
});
|
||||
|
|
@ -0,0 +1,752 @@
|
|||
import {
|
||||
SIZES,
|
||||
STORE_FETCH_ROUTES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
dragenterFiles,
|
||||
dropFiles,
|
||||
insertText,
|
||||
listenStoreFetch,
|
||||
onRpcBefore,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
waitStoreFetch,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred, advanceTime } from "@odoo/hoot-mock";
|
||||
import {
|
||||
asyncStep,
|
||||
defineActions,
|
||||
getService,
|
||||
mockService,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { DELAY_FOR_SPINNER } from "@mail/chatter/web_portal/chatter";
|
||||
import { queryFirst } from "@odoo/hoot-dom";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("simple chatter on a record", async () => {
|
||||
const pyEnv = await startServer();
|
||||
onRpcBefore((route, args) => {
|
||||
if (
|
||||
(route.startsWith("/mail") || route.startsWith("/discuss")) &&
|
||||
!STORE_FETCH_ROUTES.includes(route)
|
||||
) {
|
||||
asyncStep(`${route} - ${JSON.stringify(args)}`);
|
||||
}
|
||||
});
|
||||
listenStoreFetch(undefined, { logParams: ["mail.thread"] });
|
||||
await start();
|
||||
await waitStoreFetch(["failures", "systray_get_activities", "init_messaging"]);
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains(".o-mail-Thread");
|
||||
await waitStoreFetch(
|
||||
[
|
||||
[
|
||||
"mail.thread",
|
||||
{
|
||||
access_params: {},
|
||||
request_list: [
|
||||
"activities",
|
||||
"attachments",
|
||||
"contact_fields",
|
||||
"followers",
|
||||
"scheduledMessages",
|
||||
"suggestedRecipients",
|
||||
],
|
||||
thread_id: partnerId,
|
||||
thread_model: "res.partner",
|
||||
},
|
||||
],
|
||||
],
|
||||
{
|
||||
ignoreOrder: true,
|
||||
stepsAfter: [
|
||||
`/mail/thread/messages - {"thread_id":${partnerId},"thread_model":"res.partner","fetch_params":{"limit":30}}`,
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("can post a message on a record thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
onRpcBefore("/mail/message/post", (args) => {
|
||||
asyncStep("/mail/message/post");
|
||||
const expected = {
|
||||
context: args.context,
|
||||
post_data: {
|
||||
body: "hey",
|
||||
email_add_signature: true,
|
||||
message_type: "comment",
|
||||
subtype_xmlid: "mail.mt_comment",
|
||||
},
|
||||
thread_id: partnerId,
|
||||
thread_model: "res.partner",
|
||||
};
|
||||
expect(args).toEqual(expected);
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button", { text: "Send message" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Composer");
|
||||
await insertText(".o-mail-Composer-input", "hey");
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await click(".o-mail-Composer button[aria-label='Send']:enabled");
|
||||
await contains(".o-mail-Message");
|
||||
await waitForSteps(["/mail/message/post"]);
|
||||
});
|
||||
|
||||
test("can post a note on a record thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
onRpcBefore("/mail/message/post", (args) => {
|
||||
asyncStep("/mail/message/post");
|
||||
const expected = {
|
||||
context: args.context,
|
||||
post_data: {
|
||||
body: "hey",
|
||||
email_add_signature: true,
|
||||
message_type: "comment",
|
||||
subtype_xmlid: "mail.mt_note",
|
||||
},
|
||||
thread_id: partnerId,
|
||||
thread_model: "res.partner",
|
||||
};
|
||||
expect(args).toEqual(expected);
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button", { text: "Log note" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await click("button", { text: "Log note" });
|
||||
await contains(".o-mail-Composer");
|
||||
await insertText(".o-mail-Composer-input", "hey");
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await click(".o-mail-Composer button:enabled", { text: "Log" });
|
||||
await contains(".o-mail-Message");
|
||||
await waitForSteps(["/mail/message/post"]);
|
||||
});
|
||||
|
||||
test("No attachment loading spinner when creating records", async () => {
|
||||
await start();
|
||||
await openFormView("res.partner");
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await contains("button[aria-label='Attach files'] .fa-spin", { count: 0 });
|
||||
});
|
||||
|
||||
test("No attachment loading spinner when switching from loading record to creation of record", async () => {
|
||||
const def = new Deferred();
|
||||
const pyEnv = await startServer();
|
||||
listenStoreFetch("mail.thread", {
|
||||
async onRpc() {
|
||||
asyncStep("before mail.thread");
|
||||
await def;
|
||||
},
|
||||
});
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await advanceTime(DELAY_FOR_SPINNER);
|
||||
await contains("button[aria-label='Attach files'] .fa-spin");
|
||||
await click(".o_control_panel_main_buttons .o_form_button_create");
|
||||
await contains("button[aria-label='Attach files'] .fa-spin", { count: 0 });
|
||||
await waitForSteps(["before mail.thread"]);
|
||||
def.resolve();
|
||||
await waitStoreFetch("mail.thread");
|
||||
});
|
||||
|
||||
test("Composer toggle state is kept when switching from aside to bottom", async () => {
|
||||
await patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Form-chatter.o-aside .o-mail-Composer-input");
|
||||
await patchUiSize({ size: SIZES.LG });
|
||||
await contains(".o-mail-Form-chatter:not(.o-aside) .o-mail-Composer-input");
|
||||
});
|
||||
|
||||
test("Textarea content is kept when switching from aside to bottom", async () => {
|
||||
await patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Form-chatter.o-aside .o-mail-Composer-input");
|
||||
await insertText(".o-mail-Composer-input", "Hello world !");
|
||||
await patchUiSize({ size: SIZES.LG });
|
||||
await contains(".o-mail-Form-chatter:not(.o-aside) .o-mail-Composer-input");
|
||||
await contains(".o-mail-Composer-input", { value: "Hello world !" });
|
||||
});
|
||||
|
||||
test("Composer type is kept when switching from aside to bottom", async () => {
|
||||
await patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("button", { text: "Log note" });
|
||||
await patchUiSize({ size: SIZES.LG });
|
||||
await contains(".o-mail-Form-chatter:not(.o-aside) .o-mail-Composer-input");
|
||||
await contains("button.btn-primary", { text: "Log note" });
|
||||
await contains("button:not(.btn-primary)", { text: "Send message" });
|
||||
});
|
||||
|
||||
test("chatter: drop attachments", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const text = new File(["hello, world"], "text.txt", { type: "text/plain" });
|
||||
const text2 = new File(["hello, worldub"], "text2.txt", { type: "text/plain" });
|
||||
const text3 = new File(["hello, world"], "text3.txt", { type: "text/plain" });
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
const files = [text, text2];
|
||||
await dragenterFiles(".o-mail-Chatter", files);
|
||||
await contains(".o-Dropzone");
|
||||
await contains(".o-mail-AttachmentContainer", { count: 0 });
|
||||
await dropFiles(".o-Dropzone", files);
|
||||
await contains(".o-mail-AttachmentContainer:not(.o-isUploading)", { count: 2 });
|
||||
const extraFiles = [text3];
|
||||
await dragenterFiles(".o-mail-Chatter", extraFiles);
|
||||
await dropFiles(".o-Dropzone", extraFiles);
|
||||
await contains(".o-mail-AttachmentContainer:not(.o-isUploading)", { count: 3 });
|
||||
});
|
||||
|
||||
test("chatter: drop attachment should refresh thread data with hasParentReloadOnAttachmentsChange prop", async () => {
|
||||
await patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const textPdf = new File([new Uint8Array(1)], "text.pdf", { type: "application/pdf" });
|
||||
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<div class="o_attachment_preview" />
|
||||
<chatter reload_on_post="True" reload_on_attachment="True"/>
|
||||
</form>`,
|
||||
});
|
||||
await dragenterFiles(".o-mail-Chatter", [textPdf]);
|
||||
await dropFiles(".o-Dropzone", [textPdf]);
|
||||
await contains(".o-mail-Attachment iframe", { count: 1 });
|
||||
});
|
||||
|
||||
test("should display subject when subject isn't infered from the record", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message", { text: "Subject: Salutations, voyageurnot empty" });
|
||||
});
|
||||
|
||||
test("should not display user notification messages in chatter", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.message"].create({
|
||||
message_type: "user_notification",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Thread", { text: "The conversation is empty." });
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
});
|
||||
|
||||
test('post message with "CTRL-Enter" keyboard shortcut in chatter', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await insertText(".o-mail-Composer-input", "Test");
|
||||
triggerHotkey("control+Enter");
|
||||
await contains(".o-mail-Message");
|
||||
});
|
||||
|
||||
test("base rendering when chatter has no attachment", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
for (let i = 0; i < 60; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter");
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains(".o-mail-AttachmentBox", { count: 0 });
|
||||
await contains(".o-mail-Thread");
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
});
|
||||
|
||||
test("base rendering when chatter has no record", async () => {
|
||||
await start();
|
||||
await openFormView("res.partner");
|
||||
await contains(".o-mail-Chatter");
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains(".o-mail-AttachmentBox", { count: 0 });
|
||||
await contains(".o-mail-Chatter .o-mail-Thread");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message-author", { text: "Mitchell Admin" });
|
||||
await contains(".o-mail-Message-body", { text: "Creating a new record..." });
|
||||
await contains("button", { count: 0, text: "Load More" });
|
||||
});
|
||||
|
||||
test("base rendering when chatter has attachments", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blu.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter");
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains(".o-mail-AttachmentBox", { count: 0 });
|
||||
});
|
||||
|
||||
test("show attachment box", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blu.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter");
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await contains("button[aria-label='Attach files']", { text: "2" });
|
||||
await contains(".o-mail-AttachmentBox", { count: 0 });
|
||||
await click("button[aria-label='Attach files']");
|
||||
await contains(".o-mail-AttachmentBox");
|
||||
});
|
||||
|
||||
test("composer show/hide on log note/send message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button", { text: "Send message" });
|
||||
await contains("button", { text: "Log note" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Composer");
|
||||
expect(".o-mail-Composer-input").toBeFocused();
|
||||
await click("button", { text: "Log note" });
|
||||
await contains(".o-mail-Composer");
|
||||
expect(".o-mail-Composer-input").toBeFocused();
|
||||
await click("button", { text: "Log note" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Composer");
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
});
|
||||
|
||||
test('do not post message with "Enter" keyboard shortcut', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await insertText(".o-mail-Composer-input", "Test");
|
||||
triggerHotkey("Enter");
|
||||
// weak test, no guarantee that we waited long enough for the potential message to be posted
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
});
|
||||
|
||||
test("should not display subject when subject is the same as the thread name", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message", { text: "not empty" });
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("scroll position is kept when navigating from one record to another", async () => {
|
||||
await patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Harry Potter" });
|
||||
const partnerId_2 = pyEnv["res.partner"].create({ name: "Ron Weasley" });
|
||||
// Fill both channels with random messages in order for the scrollbar to
|
||||
// appear.
|
||||
pyEnv["mail.message"].create(
|
||||
Array(50)
|
||||
.fill(0)
|
||||
.map((_, index) => ({
|
||||
body: "Non Empty Body ".repeat(25),
|
||||
model: "res.partner",
|
||||
res_id: index < 20 ? partnerId_1 : partnerId_2,
|
||||
}))
|
||||
);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1);
|
||||
await contains(".o-mail-Message", { count: 20 });
|
||||
const clientHeight1 = queryFirst(".o-mail-Chatter:first").clientHeight; // client height might change (cause: breadcrumb)
|
||||
const scrollValue1 = queryFirst(".o-mail-Chatter:first").scrollHeight / 2;
|
||||
await contains(".o-mail-Chatter", { scroll: 0 });
|
||||
await scroll(".o-mail-Chatter", scrollValue1);
|
||||
await openFormView("res.partner", partnerId_2);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
const clientHeight2 = queryFirst(".o-mail-Chatter:first").clientHeight;
|
||||
const scrollValue2 = queryFirst(".o-mail-Chatter:first").scrollHeight / 3;
|
||||
await scroll(".o-mail-Chatter", scrollValue2);
|
||||
await openFormView("res.partner", partnerId_1);
|
||||
await contains(".o-mail-Message", { count: 20 });
|
||||
const clientHeight3 = queryFirst(".o-mail-Chatter:first").clientHeight;
|
||||
await contains(".o-mail-Chatter", { scroll: scrollValue1 - (clientHeight3 - clientHeight1) });
|
||||
await openFormView("res.partner", partnerId_2);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
const clientHeight4 = queryFirst(".o-mail-Chatter:first").clientHeight;
|
||||
await contains(".o-mail-Chatter", { scroll: scrollValue2 - (clientHeight4 - clientHeight2) });
|
||||
});
|
||||
|
||||
test("basic chatter rendering", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ display_name: "second partner" });
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o-mail-Chatter");
|
||||
});
|
||||
|
||||
test('chatter just contains "creating a new record" message during the creation of a new record after having displayed a chatter for an existing record', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const views = {
|
||||
"res.partner,false,form": `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
};
|
||||
await start({ serverData: { views } });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".o_control_panel_main_buttons .o_form_button_create");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message-body", { text: "Creating a new record..." });
|
||||
});
|
||||
|
||||
test("should display subject when subject is not the same as the default subject", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.fake"].create({ name: "Salutations, voyageur" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.fake",
|
||||
res_id: fakeId,
|
||||
subject: "Another Subject",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await contains(".o-mail-Message", { text: "Subject: Another Subjectnot empty" });
|
||||
});
|
||||
|
||||
test("should not display subject when subject is the same as the default subject", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.fake"].create({ name: "Salutations, voyageur" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.fake",
|
||||
res_id: fakeId,
|
||||
subject: "Custom Default Subject",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await contains(".o-mail-Message", { text: "not empty" });
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Custom Default Subjectnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("should not display subject when subject is the same as the thread name with custom default subject", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.fake"].create({ name: "Salutations, voyageur" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.fake",
|
||||
res_id: fakeId,
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await contains(".o-mail-Message", { text: "not empty" });
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Custom Default Subjectnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("chatter updating", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ display_name: "first partner" },
|
||||
{ display_name: "second partner" },
|
||||
]);
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId_2,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
resIds: [partnerId_1, partnerId_2],
|
||||
});
|
||||
await click(".o_pager_next");
|
||||
await contains(".o-mail-Message");
|
||||
});
|
||||
|
||||
test("chatter message actions appear only after saving the form", async () => {
|
||||
await start();
|
||||
await openFormView("res.partner");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message-actions", { count: 0 });
|
||||
await click(".o_form_button_save");
|
||||
await click("button", { text: "Send message" });
|
||||
await insertText(".o-mail-Composer-input", "hey");
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o-mail-Message-actions");
|
||||
});
|
||||
|
||||
test("post message on draft record", async () => {
|
||||
await start();
|
||||
await openFormView("res.partner", undefined, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await click("button", { text: "Send message" });
|
||||
await insertText(".o-mail-Composer-input", "Test");
|
||||
await click(".o-mail-Composer button[aria-label='Send']:enabled");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message-content", { text: "Test" });
|
||||
});
|
||||
|
||||
test("schedule activities on draft record should prompt with scheduling an activity (proceed with action)", async () => {
|
||||
const wizardOpened = new Deferred();
|
||||
mockService("action", {
|
||||
doAction(action, options) {
|
||||
if (action.res_model === "res.partner") {
|
||||
return super.doAction(...arguments);
|
||||
} else if (action.res_model === "mail.activity.schedule") {
|
||||
asyncStep("mail.activity.schedule");
|
||||
expect(action.context.active_model).toBe("res.partner");
|
||||
expect(Number(action.context.active_id)).toBeGreaterThan(0);
|
||||
options.onClose();
|
||||
wizardOpened.resolve();
|
||||
} else {
|
||||
asyncStep("Unexpected action" + action.res_model);
|
||||
}
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", undefined, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await click("button", { text: "Activity" });
|
||||
await wizardOpened;
|
||||
await waitForSteps(["mail.activity.schedule"]);
|
||||
});
|
||||
|
||||
test("upload attachment on draft record", async () => {
|
||||
const text = new File(["hello, world"], "text.text", { type: "text/plain" });
|
||||
await start();
|
||||
await openFormView("res.partner", undefined, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await contains("button[aria-label='Attach files']", { count: 0, text: "1" });
|
||||
await dragenterFiles(".o-mail-Chatter", [text]);
|
||||
await dropFiles(".o-Dropzone", [text]);
|
||||
await contains("button[aria-label='Attach files']", { text: "1" });
|
||||
});
|
||||
|
||||
test("Follower count of draft record is set to 0", async () => {
|
||||
await start();
|
||||
await openFormView("res.partner");
|
||||
await contains(".o-mail-Followers", { text: "0" });
|
||||
});
|
||||
|
||||
test("Mentions in composer should still work when using pager", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ display_name: "Partner 1" },
|
||||
{ display_name: "Partner 2" },
|
||||
]);
|
||||
await patchUiSize({ size: SIZES.LG });
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1, { resIds: [partnerId_1, partnerId_2] });
|
||||
await click("button", { text: "Log note" });
|
||||
await click(".o_pager_next");
|
||||
await insertText(".o-mail-Composer-input", "@");
|
||||
// all records in DB: Mitchell Admin | Hermit | Public user except OdooBot
|
||||
await contains(".o-mail-Composer-suggestion", { count: 3 });
|
||||
});
|
||||
|
||||
test("form views in dialogs do not have chatter", async () => {
|
||||
defineActions([
|
||||
{
|
||||
id: 1,
|
||||
name: "Partner",
|
||||
res_model: "res.partner",
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await getService("action").doAction(1);
|
||||
await contains(".o_dialog .o_form_view");
|
||||
await contains(".o-mail-Form-Chatter", { count: 0 });
|
||||
});
|
||||
|
||||
test("should display the subject even if the record name is false", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.fake"].create({ name: false });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.fake",
|
||||
res_id: fakeId,
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await contains(".o-mail-Message", { text: "Subject: Salutations, voyageurnot empty" });
|
||||
});
|
||||
|
||||
test("Update message recipients without saving", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { email: "mitchell@odoo.com" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "John Doe",
|
||||
email: "john@doe.be",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({
|
||||
name: "John Doe",
|
||||
partner_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text", { text: "John Doe" });
|
||||
await click(".o_field_many2one_selection input");
|
||||
await click(".o-autocomplete--dropdown-item", { text: "Mitchell Admin" });
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text", { text: "Mitchell Admin" });
|
||||
});
|
||||
|
||||
test("Update primary email in recipient without saving", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { email: "mitchell@odoo.com" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "John Doe",
|
||||
email: "john@doe.be",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({
|
||||
name: "Fake record",
|
||||
partner_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await click("button", { text: "Send message" });
|
||||
await insertText("div[name='email_cc'] input", "test@test.be");
|
||||
document.querySelector("div[name='email_cc'] input").blur();
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text", { text: "test@test.be" });
|
||||
});
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import {
|
||||
SIZES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { HIGHLIGHT_CLASS } from "@mail/core/common/message_search_hook";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Chatter should display search icon", async () => {
|
||||
const pyEnv = await startServer();
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("[title='Search Messages']");
|
||||
});
|
||||
|
||||
test("Click on the search icon should open the search form", async () => {
|
||||
const pyEnv = await startServer();
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("[title='Search Messages']");
|
||||
await contains(".o_searchview");
|
||||
await contains(".o_searchview_input");
|
||||
});
|
||||
|
||||
test("Search in chatter", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "empty");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessageResult .o-mail-Message");
|
||||
await click(".o-mail-MessageCard-jump");
|
||||
await contains(".o-mail-Message.o-highlighted .o-mail-Message-content", { text: "not empty" });
|
||||
});
|
||||
|
||||
test("Close button should close the search panel", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".o-mail-Chatter-topbar [title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "empty");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessageResult .o-mail-Message");
|
||||
await click(".o-mail-SearchMessageInput [title='Close']");
|
||||
await contains(".o-mail-SearchMessageInput", { count: 0 });
|
||||
});
|
||||
|
||||
test("Search in chatter should be hightligted", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "empty");
|
||||
triggerHotkey("Enter");
|
||||
await contains(`.o-mail-SearchMessageResult .o-mail-Message .${HIGHLIGHT_CLASS}`);
|
||||
});
|
||||
|
||||
test("Scrolling bottom in non-aside chatter should load more searched message", async () => {
|
||||
patchUiSize({ size: SIZES.LG }); // non-aside
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
for (let i = 0; i < 60; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessageResult .o-mail-Message", { count: 30 });
|
||||
await scroll(".o_content", "bottom");
|
||||
await contains(".o-mail-SearchMessageResult .o-mail-Message", { count: 60 });
|
||||
});
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
listenStoreFetch,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
waitStoreFetch,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Deferred, advanceTime } from "@odoo/hoot-mock";
|
||||
import { asyncStep, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { DELAY_FOR_SPINNER } from "@mail/chatter/web_portal/chatter";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("base rendering", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains("button", { text: "Send message" });
|
||||
await contains("button", { text: "Log note" });
|
||||
await contains("button", { text: "Activity" });
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await contains(".o-mail-Followers");
|
||||
});
|
||||
|
||||
test("rendering with multiple partner followers", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2, partnerId_3] = pyEnv["res.partner"].create([
|
||||
{ name: "Eden Hazard" },
|
||||
{ name: "Jean Michang" },
|
||||
{},
|
||||
]);
|
||||
pyEnv["mail.followers"].create([
|
||||
{
|
||||
partner_id: partnerId_2,
|
||||
res_id: partnerId_3,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
partner_id: partnerId_1,
|
||||
res_id: partnerId_3,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_3);
|
||||
await contains(".o-mail-Followers");
|
||||
await contains(".o-mail-Followers-button");
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Followers-dropdown");
|
||||
await contains(".o-mail-Follower", { count: 2 });
|
||||
await contains(":nth-child(1 of .o-mail-Follower)", { text: "Jean Michang" });
|
||||
await contains(":nth-child(2 of .o-mail-Follower)", { text: "Eden Hazard" });
|
||||
});
|
||||
|
||||
test("log note toggling", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button:not(.active)", { text: "Log note" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await click("button", { text: "Log note" });
|
||||
await contains("button.active", { text: "Log note" });
|
||||
await contains(".o-mail-Composer .o-mail-Composer-input[placeholder='Log an internal note…']");
|
||||
await click("button", { text: "Log note" });
|
||||
await contains("button:not(.active)", { text: "Log note" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
});
|
||||
|
||||
test("send message toggling", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button:not(.active)", { text: "Send message" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await click("button", { text: "Send message" });
|
||||
await contains("button.active", { text: "Send message" });
|
||||
await contains(".o-mail-Composer-input[placeholder='Send a message to followers…']");
|
||||
await click("button", { text: "Send message" });
|
||||
await contains("button:not(.active)", { text: "Send message" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
});
|
||||
|
||||
test("log note/send message switching", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button:not(.active)", { text: "Send message" });
|
||||
await contains("button:not(.active)", { text: "Log note" });
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await click("button", { text: "Send message" });
|
||||
await contains("button.active", { text: "Send message" });
|
||||
await contains("button:not(.active)", { text: "Log note" });
|
||||
await contains(".o-mail-Composer-input[placeholder='Send a message to followers…']");
|
||||
await click("button", { text: "Log note" });
|
||||
await contains("button:not(.active)", { text: "Send message" });
|
||||
await contains("button.active", { text: "Log note" });
|
||||
await contains(".o-mail-Composer-input[placeholder='Log an internal note…']");
|
||||
});
|
||||
|
||||
test("attachment counter without attachments", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await contains("button[aria-label='Attach files']", { count: 0, text: "0" });
|
||||
});
|
||||
|
||||
test("attachment counter with attachments", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blu.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button[aria-label='Attach files']", { text: "2" });
|
||||
});
|
||||
|
||||
test("attachment counter while loading attachments", async () => {
|
||||
const def = new Deferred();
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
listenStoreFetch("mail.thread", {
|
||||
async onRpc() {
|
||||
asyncStep("before mail.thread");
|
||||
await def;
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await advanceTime(DELAY_FOR_SPINNER);
|
||||
await contains("button[aria-label='Attach files'] .fa-spin");
|
||||
await contains("button[aria-label='Attach files']", { count: 0, text: "0" });
|
||||
await waitForSteps(["before mail.thread"]);
|
||||
def.resolve();
|
||||
await waitStoreFetch("mail.thread");
|
||||
});
|
||||
|
||||
test("attachment counter transition when attachments become loaded", async () => {
|
||||
const def = new Deferred();
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
listenStoreFetch("mail.thread", {
|
||||
async onRpc() {
|
||||
asyncStep("before mail.thread");
|
||||
await def;
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await advanceTime(DELAY_FOR_SPINNER);
|
||||
await contains("button[aria-label='Attach files'] .fa-spin");
|
||||
await waitForSteps(["before mail.thread"]);
|
||||
def.resolve();
|
||||
await waitStoreFetch("mail.thread");
|
||||
await contains("button[aria-label='Attach files'] .fa-spin", { count: 0 });
|
||||
});
|
||||
|
||||
test("attachment icon open directly the file uploader if there is no attachment yet", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter-fileUploader");
|
||||
await contains(".o-mail-AttachmentBox", { count: 0 });
|
||||
});
|
||||
|
||||
test("attachment icon open the attachment box when there is at least 1 attachment", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text/plain",
|
||||
name: "Blah.txt",
|
||||
res_id: partnerId,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await contains(".o-mail-AttachmentBox", { count: 0 });
|
||||
await contains(".o-mail-Chatter-fileUploader", { count: 0 });
|
||||
await click("button[aria-label='Attach files']");
|
||||
await contains(".o-mail-AttachmentBox");
|
||||
await contains(".o-mail-Chatter-fileUploader");
|
||||
});
|
||||
|
||||
test("composer state conserved when clicking on another topbar button", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter-topbar");
|
||||
await contains("button", { text: "Send message" });
|
||||
await contains("button", { text: "Log note" });
|
||||
await contains("button[aria-label='Attach files']");
|
||||
await click("button", { text: "Log note" });
|
||||
await contains("button.active", { text: "Log note" });
|
||||
await contains("button:not(.active)", { text: "Send message" });
|
||||
await click(".o-mail-Chatter-topbar button[aria-label='Attach files']");
|
||||
await contains("button.active", { text: "Log note" });
|
||||
await contains("button:not(.active)", { text: "Send message" });
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("base rendering follow, edit subscription and unfollow button", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const threadId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", threadId);
|
||||
await contains(".o-mail-Followers-counter", { text: "0" });
|
||||
await contains("[title='Show Followers'] .fa-user-o");
|
||||
await click("[title='Show Followers']");
|
||||
await click(".o-dropdown-item", { text: "Follow" });
|
||||
await contains(".o-mail-Followers-counter", { text: "1" });
|
||||
await contains("[title='Show Followers'] .fa-user");
|
||||
await click("[title='Show Followers']");
|
||||
await contains(".o-mail-Followers-dropdown");
|
||||
await click("[title='Edit subscription']");
|
||||
await contains(".o-mail-Followers-dropdown", { count: 0 });
|
||||
await click("[title='Show Followers']");
|
||||
await click(".o-dropdown-item", { text: "Unfollow" });
|
||||
await contains(".o-mail-Followers-counter", { text: "0" });
|
||||
await contains("[title='Show Followers'] .fa-user-o");
|
||||
});
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
editInput,
|
||||
onRpcBefore,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
import { asyncStep, mockService, onRpc, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("base rendering not editable", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [threadId, partnerId] = pyEnv["res.partner"].create([
|
||||
{ hasWriteAccess: false },
|
||||
{ hasWriteAccess: false },
|
||||
]);
|
||||
pyEnv["mail.followers"].create({
|
||||
is_active: true,
|
||||
partner_id: partnerId,
|
||||
res_id: threadId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", threadId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Follower");
|
||||
await contains(".o-mail-Follower-details");
|
||||
await contains(".o-mail-Follower-avatar");
|
||||
await contains(".o-mail-Follower-action", { count: 0 });
|
||||
});
|
||||
|
||||
test("base rendering editable", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]);
|
||||
pyEnv["mail.followers"].create({
|
||||
is_active: true,
|
||||
partner_id: partnerId,
|
||||
res_id: threadId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", threadId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Follower");
|
||||
await contains(".o-mail-Follower-details");
|
||||
await contains(".o-mail-Follower-avatar");
|
||||
await contains(".o-mail-Follower");
|
||||
await contains("[title='Edit subscription']");
|
||||
await contains("[title='Remove this follower']");
|
||||
});
|
||||
|
||||
test("click on partner follower details", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]);
|
||||
pyEnv["mail.followers"].create({
|
||||
is_active: true,
|
||||
partner_id: partnerId,
|
||||
res_id: threadId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
const openFormDef = new Deferred();
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
if (action?.res_id !== partnerId) {
|
||||
return super.doAction(...arguments);
|
||||
}
|
||||
asyncStep("do_action");
|
||||
expect(action.res_id).toBe(partnerId);
|
||||
expect(action.res_model).toBe("res.partner");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
openFormDef.resolve();
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", threadId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Follower");
|
||||
await contains(".o-mail-Follower-details");
|
||||
await click(".o-mail-Follower-details:first");
|
||||
await openFormDef;
|
||||
await waitForSteps(["do_action"]); // redirect to partner profile
|
||||
});
|
||||
|
||||
test("click on edit follower", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]);
|
||||
pyEnv["mail.followers"].create({
|
||||
is_active: true,
|
||||
partner_id: partnerId,
|
||||
res_id: threadId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpcBefore("/mail/read_subscription_data", () => asyncStep("fetch_subtypes"));
|
||||
await start();
|
||||
await openFormView("res.partner", threadId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Follower");
|
||||
await contains("[title='Edit subscription']");
|
||||
await click("[title='Edit subscription']");
|
||||
await contains(".o-mail-Follower", { count: 0 });
|
||||
await waitForSteps(["fetch_subtypes"]);
|
||||
await contains(".o-mail-FollowerSubtypeDialog");
|
||||
});
|
||||
|
||||
test("edit follower and close subtype dialog", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]);
|
||||
pyEnv["mail.followers"].create({
|
||||
is_active: true,
|
||||
partner_id: partnerId,
|
||||
res_id: threadId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpcBefore("/mail/read_subscription_data", () => asyncStep("fetch_subtypes"));
|
||||
await start();
|
||||
await openFormView("res.partner", threadId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Follower");
|
||||
await contains("[title='Edit subscription']");
|
||||
await click("[title='Edit subscription']");
|
||||
await contains(".o-mail-FollowerSubtypeDialog");
|
||||
await waitForSteps(["fetch_subtypes"]);
|
||||
await click(".o-mail-FollowerSubtypeDialog button", { text: "Cancel" });
|
||||
await contains(".o-mail-FollowerSubtypeDialog", { count: 0 });
|
||||
});
|
||||
|
||||
test("remove a follower in a dirty form view", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]);
|
||||
pyEnv["discuss.channel"].create({ name: "General", display_name: "General" });
|
||||
pyEnv["mail.followers"].create({
|
||||
is_active: true,
|
||||
partner_id: partnerId,
|
||||
res_id: threadId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", threadId, {
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="channel_ids" widget="many2many_tags"/>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await click(".o_field_many2many_tags[name='channel_ids'] input");
|
||||
await click(".dropdown-item", { text: "General" });
|
||||
await contains(".o_tag", { text: "General" });
|
||||
await contains(".o-mail-Followers-counter", { text: "1" });
|
||||
await editInput(document.body, ".o_field_char[name=name] input", "some value");
|
||||
await click(".o-mail-Followers-button");
|
||||
await click("[title='Remove this follower']");
|
||||
await contains(".o-mail-Followers-counter", { text: "0" });
|
||||
await contains(".o_field_char[name=name] input", { value: "some value" });
|
||||
await contains(".o_tag", { text: "General" });
|
||||
});
|
||||
|
||||
test("removing a follower should reload form view", async function () {
|
||||
const pyEnv = await startServer();
|
||||
const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]);
|
||||
pyEnv["mail.followers"].create({
|
||||
is_active: true,
|
||||
partner_id: partnerId,
|
||||
res_id: threadId,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
onRpc("res.partner", "web_read", ({ args }) => asyncStep(`read ${args[0][0]}`));
|
||||
await start();
|
||||
await openFormView("res.partner", threadId);
|
||||
await contains(".o-mail-Followers-button");
|
||||
await waitForSteps([`read ${threadId}`]);
|
||||
await click(".o-mail-Followers-button");
|
||||
await click("[title='Remove this follower']");
|
||||
await contains(".o-mail-Followers-counter", { text: "0" });
|
||||
await waitForSteps([`read ${threadId}`]);
|
||||
});
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openFormView,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { tick } from "@odoo/hoot-dom";
|
||||
import { asyncStep, mockService, serverState, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("base rendering not editable", async () => {
|
||||
await start();
|
||||
await openFormView("res.partner", undefined, {});
|
||||
await contains(".o-mail-Followers");
|
||||
await contains(".o-mail-Followers-button:disabled");
|
||||
await contains(".o-mail-Followers-dropdown", { count: 0 });
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Followers-dropdown", { count: 0 });
|
||||
});
|
||||
|
||||
test("base rendering editable", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Followers");
|
||||
await contains(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Followers-button:first:enabled");
|
||||
await contains(".o-mail-Followers-dropdown", { count: 0 });
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Followers-dropdown");
|
||||
});
|
||||
|
||||
test('click on "add followers" button', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2, partnerId_3] = pyEnv["res.partner"].create([
|
||||
{ name: "Partner1" },
|
||||
{ name: "François Perusse" },
|
||||
{ name: "Partner3" },
|
||||
]);
|
||||
pyEnv["mail.followers"].create({
|
||||
partner_id: partnerId_2,
|
||||
email: "bla@bla.bla",
|
||||
is_active: true,
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
mockService("action", {
|
||||
doAction(action, options) {
|
||||
if (action?.res_model !== "mail.followers.edit") {
|
||||
return super.doAction(...arguments);
|
||||
}
|
||||
asyncStep("action:open_view");
|
||||
expect(action.context.default_res_model).toBe("res.partner");
|
||||
expect(action.context.default_res_ids).toEqual([partnerId_1]);
|
||||
expect(action.res_model).toBe("mail.followers.edit");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
pyEnv["mail.followers"].create({
|
||||
partner_id: partnerId_3,
|
||||
email: "bla@bla.bla",
|
||||
is_active: true,
|
||||
name: "Wololo",
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
options.onClose();
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1);
|
||||
await contains(".o-mail-Followers");
|
||||
await contains(".o-mail-Followers-counter", { text: "1" });
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Followers-dropdown");
|
||||
await click("a", { text: "Add Followers" });
|
||||
await contains(".o-mail-Followers-dropdown", { count: 0 });
|
||||
await waitForSteps(["action:open_view"]);
|
||||
await contains(".o-mail-Followers-counter", { text: "2" });
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Follower", { count: 2 });
|
||||
await contains(":nth-child(1 of .o-mail-Follower)", { text: "François Perusse" });
|
||||
await contains(":nth-child(2 of .o-mail-Follower)", { text: "Partner3" });
|
||||
});
|
||||
|
||||
test("click on remove follower", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "Partner1" },
|
||||
{ name: "Partner2" },
|
||||
]);
|
||||
pyEnv["mail.followers"].create({
|
||||
partner_id: partnerId_2,
|
||||
email: "bla@bla.bla",
|
||||
is_active: true,
|
||||
name: "Wololo",
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains(".o-mail-Follower");
|
||||
await click("[title='Remove this follower']");
|
||||
await contains(".o-mail-Follower", { count: 0 });
|
||||
await contains(".o-mail-Followers-dropdown");
|
||||
});
|
||||
|
||||
test("Load 100 followers at once", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerIds = pyEnv["res.partner"].create(
|
||||
[...Array(210).keys()].map((i) => ({ display_name: `Partner${i}`, name: `Partner${i}` }))
|
||||
);
|
||||
pyEnv["mail.followers"].create(
|
||||
[...Array(210).keys()].map((i) => ({
|
||||
is_active: true,
|
||||
partner_id: i === 0 ? serverState.partnerId : partnerIds[i],
|
||||
res_id: partnerIds[0],
|
||||
res_model: "res.partner",
|
||||
}))
|
||||
);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerIds[0]);
|
||||
await contains("button[title='Show Followers']", { text: "210" });
|
||||
await click("[title='Show Followers']");
|
||||
await contains(".o-mail-Follower", { count: 100 });
|
||||
await contains(".o-mail-Followers-dropdown", { text: "Load more" });
|
||||
await scroll(".o-mail-Followers-dropdown", "bottom");
|
||||
await contains(".o-mail-Follower", { count: 200 });
|
||||
await tick(); // give enough time for the useVisible hook to register load more as hidden
|
||||
await scroll(".o-mail-Followers-dropdown", "bottom");
|
||||
await contains(".o-mail-Follower", { count: 209 });
|
||||
await contains(".o-mail-Followers-dropdown span", { count: 0, text: "Load more" });
|
||||
});
|
||||
|
||||
test("Load 100 recipients at once", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerIds = pyEnv["res.partner"].create(
|
||||
[...Array(210).keys()].map((i) => ({
|
||||
display_name: `Partner${i}`,
|
||||
name: `Partner${i}`,
|
||||
email: `partner${i}@example.com`,
|
||||
}))
|
||||
);
|
||||
pyEnv["mail.followers"].create(
|
||||
[...Array(210).keys()].map((i) => ({
|
||||
is_active: true,
|
||||
partner_id: i === 0 ? serverState.partnerId : partnerIds[i],
|
||||
res_id: partnerIds[0],
|
||||
res_model: "res.partner",
|
||||
}))
|
||||
);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerIds[0]);
|
||||
await contains("button[title='Show Followers']", { text: "210" });
|
||||
});
|
||||
|
||||
test('Show "Add follower" and subtypes edition/removal buttons on all followers if user has write access', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "Partner1" },
|
||||
{ name: "Partner2" },
|
||||
]);
|
||||
pyEnv["mail.followers"].create([
|
||||
{
|
||||
is_active: true,
|
||||
partner_id: serverState.partnerId,
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
is_active: true,
|
||||
partner_id: partnerId_2,
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains("a", { text: "Add Followers" });
|
||||
await contains(":nth-child(1 of .o-mail-Follower)", {
|
||||
contains: [["[title='Edit subscription']"], ["[title='Remove this follower']"]],
|
||||
});
|
||||
});
|
||||
|
||||
test('Show "No Followers" dropdown-item if there are no followers and user does not have write access', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ hasWriteAccess: false });
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await contains("div.disabled", { text: "No Followers" });
|
||||
});
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("simplest layout of a followed subtype", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const subtypeId = pyEnv["mail.message.subtype"].create({
|
||||
default: true,
|
||||
name: "TestSubtype",
|
||||
});
|
||||
pyEnv["mail.followers"].create({
|
||||
display_name: "François Perusse",
|
||||
partner_id: serverState.partnerId,
|
||||
res_model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
subtype_ids: [subtypeId],
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await click("[title='Edit subscription']");
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] label`,
|
||||
{ text: "TestSubtype" }
|
||||
);
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] input[type='checkbox']:checked`
|
||||
);
|
||||
});
|
||||
|
||||
test("simplest layout of a not followed subtype", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const subtypeId = pyEnv["mail.message.subtype"].create({
|
||||
default: true,
|
||||
name: "TestSubtype",
|
||||
});
|
||||
pyEnv["mail.followers"].create({
|
||||
display_name: "François Perusse",
|
||||
partner_id: serverState.partnerId,
|
||||
res_model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await click("[title='Edit subscription']");
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] input[type='checkbox']:not(:checked)`
|
||||
);
|
||||
});
|
||||
|
||||
test("toggle follower subtype checkbox", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const subtypeId = pyEnv["mail.message.subtype"].create({
|
||||
default: true,
|
||||
name: "TestSubtype",
|
||||
});
|
||||
pyEnv["mail.followers"].create({
|
||||
display_name: "François Perusse",
|
||||
partner_id: serverState.partnerId,
|
||||
res_model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await click("[title='Edit subscription']");
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] input[type='checkbox']:not(:checked)`
|
||||
);
|
||||
await click(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] input[type='checkbox']`
|
||||
);
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] input[type='checkbox']:checked`
|
||||
);
|
||||
await click(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] input[type='checkbox']`
|
||||
);
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId}'] input[type='checkbox']:not(:checked)`
|
||||
);
|
||||
});
|
||||
|
||||
test("follower subtype apply", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const subtypeId1 = pyEnv["mail.message.subtype"].create({
|
||||
default: true,
|
||||
name: "TestSubtype1",
|
||||
});
|
||||
const subtypeId2 = pyEnv["mail.message.subtype"].create({
|
||||
default: true,
|
||||
name: "TestSubtype2",
|
||||
});
|
||||
pyEnv["mail.followers"].create({
|
||||
display_name: "François Perusse",
|
||||
partner_id: serverState.partnerId,
|
||||
res_model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
subtype_ids: [subtypeId1],
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click(".o-mail-Followers-button");
|
||||
await click("[title='Edit subscription']");
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId1}'] input[type='checkbox']:checked`
|
||||
);
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId2}'] input[type='checkbox']:not(:checked)`
|
||||
);
|
||||
await click(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId1}'] input[type='checkbox']`
|
||||
);
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId1}'] input[type='checkbox']:not(:checked)`
|
||||
);
|
||||
await click(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId2}'] input[type='checkbox']`
|
||||
);
|
||||
await contains(
|
||||
`.o-mail-FollowerSubtypeDialog-subtype[data-follower-subtype-id='${subtypeId2}'] input[type='checkbox']:checked`
|
||||
);
|
||||
await click(".modal-footer button", { text: "Apply" });
|
||||
await contains(".o_notification", {
|
||||
text: "The subscription preferences were successfully applied.",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
import {
|
||||
SIZES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { mockService, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test.skip("Form view not scrolled when switching record", async () => {
|
||||
// FIXME: test passed in test environment but in practice scroll are reset to 0
|
||||
// HOOT matches behaviour in prod and shows tests not passing as expected
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{
|
||||
description: [...Array(60).keys()].join("\n"),
|
||||
display_name: "Partner 1",
|
||||
},
|
||||
{
|
||||
description: [...Array(60).keys()].join("\n"),
|
||||
display_name: "Partner 2",
|
||||
},
|
||||
]);
|
||||
const messages = [...Array(60).keys()].map((id) => {
|
||||
return {
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: id < 29 ? partnerId_1 : partnerId_2,
|
||||
};
|
||||
});
|
||||
pyEnv["mail.message"].create(messages);
|
||||
patchUiSize({ size: SIZES.LG });
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
resIds: [partnerId_1, partnerId_2],
|
||||
});
|
||||
await contains(".o-mail-Message", { count: 29 });
|
||||
await contains(".o_content", { scroll: 0 });
|
||||
await scroll(".o_content", 150);
|
||||
await click(".o_pager_next");
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await contains(".o_content", { scroll: 150 });
|
||||
await scroll(".o_content", 0);
|
||||
await click(".o_pager_previous");
|
||||
await contains(".o-mail-Message", { count: 29 });
|
||||
await contains(".o_content", { scroll: 0 });
|
||||
});
|
||||
|
||||
test("Attachments that have been unlinked from server should be visually unlinked from record", async () => {
|
||||
// Attachments that have been fetched from a record at certain time and then
|
||||
// removed from the server should be reflected on the UI when the current
|
||||
// partner accesses this record again.
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ display_name: "Partner1" },
|
||||
{ display_name: "Partner2" },
|
||||
]);
|
||||
const [attachmentId_1] = pyEnv["ir.attachment"].create([
|
||||
{
|
||||
mimetype: "text.txt",
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
{
|
||||
mimetype: "text.txt",
|
||||
res_id: partnerId_1,
|
||||
res_model: "res.partner",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId_1, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
resId: partnerId_1,
|
||||
resIds: [partnerId_1, partnerId_2],
|
||||
});
|
||||
await contains("button[aria-label='Attach files']", { text: "2" });
|
||||
// The attachment links are updated on (re)load,
|
||||
// so using pager is a way to reload the record "Partner1".
|
||||
await click(".o_pager_next");
|
||||
await contains("button[aria-label='Attach files']:not(:has(sup))");
|
||||
// Simulate unlinking attachment 1 from Partner 1.
|
||||
pyEnv["ir.attachment"].write([attachmentId_1], { res_id: 0 });
|
||||
await click(".o_pager_previous");
|
||||
await contains("button[aria-label='Attach files']", { text: "1" });
|
||||
});
|
||||
|
||||
test("ellipsis button is not duplicated when switching from read to edit mode", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
// "data-o-mail-quote" added by server is intended to be compacted in ellipsis block
|
||||
body: `
|
||||
<div>
|
||||
Dear Joel Willis,<br>
|
||||
Thank you for your enquiry.<br>
|
||||
If you have any questions, please let us know.
|
||||
<br><br>
|
||||
Thank you,<br>
|
||||
<div data-o-mail-quote="1">-- <br data-o-mail-quote="1">
|
||||
System
|
||||
</div>
|
||||
</div>`,
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o-mail-Chatter");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-ellipsis");
|
||||
});
|
||||
|
||||
test("[TECHNICAL] unfolded ellipsis button should not fold on message click besides that button", async () => {
|
||||
// message click triggers a re-render. Before writing of this test, the
|
||||
// insertion of ellipsis button were done during render. This meant
|
||||
// any re-render would re-insert the ellipsis button. If some buttons
|
||||
// were unfolded, any re-render would fold them again.
|
||||
//
|
||||
// This previous behavior is undesirable, and results to bothersome UX
|
||||
// such as inability to copy/paste unfolded message content due to click
|
||||
// from text selection automatically folding all ellipsis buttons.
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ display_name: "Someone" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
// "data-o-mail-quote" added by server is intended to be compacted in ellipsis block
|
||||
body: `
|
||||
<div>
|
||||
Dear Joel Willis,<br>
|
||||
Thank you for your enquiry.<br>
|
||||
If you have any questions, please let us know.
|
||||
<br><br>
|
||||
Thank you,<br>
|
||||
<span data-o-mail-quote="1">-- <br data-o-mail-quote="1">
|
||||
System
|
||||
</span>
|
||||
</div>`,
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o-mail-Message-body span").toHaveCount(0);
|
||||
await click(".o-mail-ellipsis");
|
||||
expect(".o-mail-Message-body span").toHaveText('--\nSystem')
|
||||
await click(".o-mail-Message");
|
||||
expect(".o-mail-Message-body span").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("ellipsis button on message of type notification", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
// "data-o-mail-quote" enables ellipsis block
|
||||
body: `
|
||||
<div>
|
||||
Dear Joel Willis,<br>
|
||||
Thank you for your enquiry.<br>
|
||||
If you have any questions, please let us know.
|
||||
<br><br>
|
||||
Thank you,<br>
|
||||
<span data-o-mail-quote="1">-- <br data-o-mail-quote="1">
|
||||
System
|
||||
</span>
|
||||
</div>`,
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
message_type: "notification",
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId, {
|
||||
arch: `
|
||||
<form string="Partners">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o-mail-ellipsis");
|
||||
});
|
||||
|
||||
test("read more/less should appear only once for the signature", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
|
||||
mockService("action", {
|
||||
doAction(action, { onClose }) {
|
||||
if (action.name === "Compose Email") {
|
||||
// Simulate message post of full composer
|
||||
pyEnv["mail.message"].create({
|
||||
body: action.context.default_body.toString(),
|
||||
model: action.context.default_model,
|
||||
res_id: action.context.default_res_ids[0],
|
||||
});
|
||||
return onClose(undefined);
|
||||
}
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// Yes you can get this kind of signature by playing with the html editor
|
||||
pyEnv["res.users"].write(serverState.userId, {
|
||||
signature: `
|
||||
<div>
|
||||
<span data-o-mail-quote="1">
|
||||
--
|
||||
</span>
|
||||
</div>
|
||||
<div data-o-mail-quote="1">
|
||||
Signature !
|
||||
</div>
|
||||
<div>
|
||||
<br data-o-mail-quote="1">
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Chatter");
|
||||
await click(".o-mail-Chatter-sendMessage");
|
||||
await insertText(".o-mail-Composer-input", "Example Body");
|
||||
await click("[name='open-full-composer']");
|
||||
await contains(".o-mail-Message-body", { text: "Example Body", count: 1 });
|
||||
expect(".o-mail-Message .o-signature-container button.o-mail-ellipsis").toHaveCount(1);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,303 @@
|
|||
import { setSelection } from "@html_editor/../tests/_helpers/selection";
|
||||
import { insertText } from "@html_editor/../tests/_helpers/user_actions";
|
||||
import { FileSelector } from "@html_editor/main/media/media_dialog/file_selector";
|
||||
import { uploadService } from "@html_editor/main/media/media_dialog/upload_progress_toast/upload_service";
|
||||
import { HtmlComposerMessageField } from "@mail/views/web/fields/html_composer_message_field/html_composer_message_field";
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
manuallyDispatchProgrammaticEvent,
|
||||
press,
|
||||
queryAll,
|
||||
queryAllTexts,
|
||||
queryOne,
|
||||
waitFor,
|
||||
waitForNone,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
makeMockServer,
|
||||
mockService,
|
||||
mountView,
|
||||
mountViewInDialog,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import {
|
||||
click,
|
||||
defineMailModels,
|
||||
mailModels,
|
||||
openFormView,
|
||||
openView,
|
||||
registerArchs,
|
||||
start,
|
||||
startServer,
|
||||
} from "../mail_test_helpers";
|
||||
|
||||
// Need this hack to use the arch in mountView(...)
|
||||
mailModels.MailComposeMessage._views = {};
|
||||
|
||||
defineMailModels([]);
|
||||
|
||||
let htmlEditor;
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(HtmlComposerMessageField.prototype, {
|
||||
onEditorLoad(editor) {
|
||||
htmlEditor = editor;
|
||||
return super.onEditorLoad(...arguments);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("media dialog: upload", async function () {
|
||||
const isUploaded = new Deferred();
|
||||
patchWithCleanup(FileSelector.prototype, {
|
||||
async onUploaded() {
|
||||
await super.onUploaded(...arguments);
|
||||
isUploaded.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
mockService("upload", uploadService);
|
||||
|
||||
const { env } = await makeMockServer();
|
||||
const resId = env["mail.compose.message"].create({
|
||||
display_name: "Some Composer",
|
||||
body: "Hello",
|
||||
attachment_ids: [],
|
||||
});
|
||||
|
||||
let newAttachmentId;
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step("web_save");
|
||||
const createVals = args[1];
|
||||
expect(createVals.attachment_ids[0][0]).toBe(4); // link command
|
||||
expect(createVals.attachment_ids[0][1]).toBe(newAttachmentId); // on attachment id "5"
|
||||
});
|
||||
onRpc("/html_editor/attachment/add_data", () => {
|
||||
const attachment = {
|
||||
name: "test.jpg",
|
||||
description: false,
|
||||
mimetype: "image/jpeg",
|
||||
checksum: "7951a43bbfb08fd742224ada280913d1897b89ab",
|
||||
url: false,
|
||||
type: "binary",
|
||||
res_id: 0,
|
||||
res_model: "mail.compose.message",
|
||||
public: false,
|
||||
access_token: false,
|
||||
image_src: "/web/image/1-a0e63e61/test.jpg",
|
||||
image_width: 1,
|
||||
image_height: 1,
|
||||
original_id: false,
|
||||
};
|
||||
newAttachmentId = env["ir.attachment"].create(attachment);
|
||||
attachment.id = newAttachmentId;
|
||||
return attachment;
|
||||
});
|
||||
|
||||
onRpc("ir.attachment", "generate_access_token", () => ["129a52e1-6bf2-470a-830e-8e368b022e13"]);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resId,
|
||||
resModel: "mail.compose.message",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="body" type="html" widget="html_composer_message"/>
|
||||
<field name="attachment_ids" widget="many2many_binary"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const anchorNode = queryOne(".odoo-editor-editable div.o-paragraph");
|
||||
setSelection({ anchorNode, anchorOffset: 0 });
|
||||
|
||||
// Open media dialog
|
||||
await animationFrame();
|
||||
await insertText(htmlEditor, "/image");
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
|
||||
// upload test
|
||||
const fileInputs = queryAll(".o_select_media_dialog input.d-none.o_file_input");
|
||||
const fileB64 =
|
||||
"/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==";
|
||||
const fileBytes = new Uint8Array(
|
||||
atob(fileB64)
|
||||
.split("")
|
||||
.map((char) => char.charCodeAt(0))
|
||||
);
|
||||
// redefine 'files' so we can put mock data in through js
|
||||
fileInputs.forEach((input) =>
|
||||
Object.defineProperty(input, "files", {
|
||||
value: [new File(fileBytes, "test.jpg", { type: "image/jpeg" })],
|
||||
})
|
||||
);
|
||||
fileInputs.forEach((input) => {
|
||||
manuallyDispatchProgrammaticEvent(input, "change");
|
||||
});
|
||||
expect("[name='attachment_ids'] .o_attachment[title='test.jpg']").toHaveCount(0);
|
||||
|
||||
await isUploaded;
|
||||
await animationFrame();
|
||||
expect("[name='attachment_ids'] .o_attachment[title='test.jpg']").toHaveCount(1);
|
||||
|
||||
await contains(".o_form_button_save").click();
|
||||
await expect.waitForSteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("mention a partner", async () => {
|
||||
onRpc("res.partner", "get_mention_suggestions", ({ kwargs }) => {
|
||||
expect.step(`get_mention_suggestions: ${kwargs.search}`);
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
registerArchs({
|
||||
"mail.compose.message,false,form": `
|
||||
<form>
|
||||
<field name="body" type="html" widget="html_composer_message"/>
|
||||
</form>`,
|
||||
});
|
||||
const composerId = pyEnv["mail.compose.message"].create({
|
||||
subject: "Greetings",
|
||||
body: "<p><br></p>",
|
||||
model: "res.partner",
|
||||
});
|
||||
await start();
|
||||
await openView({
|
||||
res_model: "mail.compose.message",
|
||||
res_id: composerId,
|
||||
views: [["mail.compose.message,false,form", "form"]],
|
||||
});
|
||||
const anchorNode = queryOne(`.odoo-editor-editable p`);
|
||||
setSelection({ anchorNode, anchorOffset: 0 });
|
||||
await insertText(htmlEditor, "@");
|
||||
await animationFrame();
|
||||
expect(".overlay .search input[placeholder='Search for a user...']").toBeFocused();
|
||||
expect(".overlay .o-mail-NavigableList .o-mail-NavigableList-item").toHaveCount(0);
|
||||
|
||||
await press("a");
|
||||
await waitFor(".overlay .o-mail-NavigableList .o-mail-NavigableList-item");
|
||||
expect(queryAllTexts(".overlay .o-mail-NavigableList .o-mail-NavigableList-item")).toEqual([
|
||||
"Mitchell Admin",
|
||||
]);
|
||||
expect.verifySteps(["get_mention_suggestions: a"]);
|
||||
|
||||
await press("enter");
|
||||
expect("[name='body'] .odoo-editor-editable").toHaveInnerHTML(`
|
||||
<p>
|
||||
<a target="_blank" data-oe-protected="true" contenteditable="false" href="https://www.hoot.test/odoo/res.partner/17" class="o_mail_redirect" data-oe-id="17" data-oe-model="res.partner">
|
||||
@Mitchell Admin
|
||||
</a>
|
||||
</p>`);
|
||||
});
|
||||
|
||||
test("mention a channel", async () => {
|
||||
onRpc("discuss.channel", "get_mention_suggestions", ({ kwargs }) => {
|
||||
expect.step(`get_mention_suggestions: ${kwargs.search}`);
|
||||
});
|
||||
await mountViewInDialog({
|
||||
type: "form",
|
||||
resModel: "mail.compose.message",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="body" type="html" widget="html_composer_message"/>
|
||||
</form>`,
|
||||
});
|
||||
const anchorNode = queryOne(`[name='body'] .odoo-editor-editable div.o-paragraph`);
|
||||
setSelection({ anchorNode, anchorOffset: 0 });
|
||||
await insertText(htmlEditor, "#");
|
||||
await animationFrame();
|
||||
expect(".overlay .search input[placeholder='Search for a channel...']").toBeFocused();
|
||||
expect(".overlay .o-mail-NavigableList .o-mail-NavigableList-item").toHaveCount(0);
|
||||
|
||||
await press("a");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["get_mention_suggestions: a"]);
|
||||
});
|
||||
|
||||
describe("Remove attachments", () => {
|
||||
beforeEach(() => {
|
||||
mailModels.MailComposeMessage._views = {
|
||||
"form,false": `
|
||||
<form js_class="mail_composer_form">
|
||||
<field name="body" type="html" widget="html_composer_message"/>
|
||||
<field name="attachment_ids" widget="mail_composer_attachment_list"/>
|
||||
</form>`,
|
||||
};
|
||||
});
|
||||
|
||||
test("should remove file from html editor if removed from attachment list", async () => {
|
||||
mockService("uploadLocalFiles", {
|
||||
async upload() {
|
||||
expect.step("File Uploaded");
|
||||
return [{ id: 1, name: "file.txt", public: true, checksum: "123" }];
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click("button", { text: "Log note" });
|
||||
await click("button[title='Open Full Composer']");
|
||||
await waitFor(".odoo-editor-editable");
|
||||
const anchorNode = queryOne(".odoo-editor-editable div.o-paragraph");
|
||||
setSelection({ anchorNode, anchorOffset: 0 });
|
||||
await insertText(htmlEditor, "/file");
|
||||
await press("Enter");
|
||||
await expect.waitForSteps(["File Uploaded"]);
|
||||
await waitFor("[name='attachment_ids'] a:contains('file.txt')");
|
||||
await waitFor(".odoo-editor-editable .o_file_box:has(a:contains('file.txt'))");
|
||||
await click("[name='attachment_ids'] button:has(i.fa-times)");
|
||||
await waitForNone("[name='attachment_ids'] a:contains('file.txt')");
|
||||
await waitForNone(".odoo-editor-editable .o_file_box:has(a:contains('file.txt'))");
|
||||
});
|
||||
|
||||
test("should remove image from html editor if removed from attachment list", async () => {
|
||||
patchWithCleanup(FileSelector.prototype, {
|
||||
async onUploaded() {
|
||||
await super.onUploaded(...arguments);
|
||||
expect.step("Image Uploaded");
|
||||
},
|
||||
});
|
||||
onRpc("/html_editor/attachment/add_data", () => ({
|
||||
id: 1,
|
||||
name: "test.jpg",
|
||||
description: false,
|
||||
mimetype: "image/jpeg",
|
||||
checksum: "7951a43bbfb08fd742224ada280913d1897b89ab",
|
||||
url: false,
|
||||
type: "binary",
|
||||
res_id: 0,
|
||||
res_model: "mail.compose.message",
|
||||
public: false,
|
||||
access_token: false,
|
||||
image_src: "/web/image/1-a0e63e61/test.jpg",
|
||||
image_width: 1,
|
||||
image_height: 1,
|
||||
original_id: false,
|
||||
}));
|
||||
onRpc("ir.attachment", "generate_access_token", () => [
|
||||
"129a52e1-6bf2-470a-830e-8e368b022e13",
|
||||
]);
|
||||
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click("button", { text: "Log note" });
|
||||
await click("button[title='Open Full Composer']");
|
||||
await waitFor(".odoo-editor-editable");
|
||||
const anchorNode = queryOne(".odoo-editor-editable div.o-paragraph");
|
||||
setSelection({ anchorNode, anchorOffset: 0 });
|
||||
await insertText(htmlEditor, "/image");
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
const fileInput = queryOne(".o_select_media_dialog input.d-none.o_file_input");
|
||||
Object.defineProperty(fileInput, "files", {
|
||||
value: [new File([], "test.jpg", { type: "image/jpeg" })],
|
||||
});
|
||||
manuallyDispatchProgrammaticEvent(fileInput, "change");
|
||||
await expect.waitForSteps(["Image Uploaded"]);
|
||||
await waitFor("[name='attachment_ids'] a:contains('test.jpg')");
|
||||
await waitFor(".odoo-editor-editable img[data-attachment-id='1']");
|
||||
await click("[name='attachment_ids'] button:has(i.fa-times)");
|
||||
await waitForNone("[name='attachment_ids'] a:contains('test.jpg')");
|
||||
await waitForNone(".odoo-editor-editable img[data-attachment-id='1']");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
onRpcBefore,
|
||||
openFormView,
|
||||
registerArchs,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred, tick } from "@odoo/hoot-mock";
|
||||
import { asyncStep, mockService, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
const archs = {
|
||||
"res.fake,false,form": `
|
||||
<form string="Fake">
|
||||
<sheet></sheet>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
"res.partner,false,form": `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<field name="name"/>
|
||||
<field name="email"/>
|
||||
<field name="phone"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
test("Show 'Followers only' placeholder for recipients input when no recipient", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "test name 1", email: "test1@odoo.com" });
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-RecipientsInput .o-autocomplete--input[placeholder='Followers only']");
|
||||
});
|
||||
|
||||
test("Opening full composer in 'send message' mode should copy selected suggested recipients", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "John Jane",
|
||||
email: "john@jane.be",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({
|
||||
email_cc: "john@test.be",
|
||||
phone: "123456789",
|
||||
partner_ids: [partnerId],
|
||||
});
|
||||
const def = new Deferred();
|
||||
mockService("action", {
|
||||
async doAction(action) {
|
||||
if (action?.res_model === "res.fake") {
|
||||
return super.doAction(...arguments);
|
||||
}
|
||||
asyncStep("do-action");
|
||||
expect(action.name).toBe("Compose Email");
|
||||
expect(action.context.default_subtype_xmlid).toBe("mail.mt_comment");
|
||||
expect(action.context.default_partner_ids).toHaveLength(2);
|
||||
const [johnTestPartnerId] = pyEnv["res.partner"].search([
|
||||
["email", "=", "john@test.be"],
|
||||
]);
|
||||
expect(action.context.default_partner_ids).toEqual([johnTestPartnerId, partnerId]);
|
||||
def.resolve();
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(John Jane)");
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
|
||||
await click("button[title='Open Full Composer']");
|
||||
await def;
|
||||
await waitForSteps(["do-action"]);
|
||||
});
|
||||
|
||||
test("Opening full composer in 'log note' mode should not copy selected suggested recipients", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "John Jane",
|
||||
email: "john@jane.be",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({
|
||||
email_cc: "john@test.be",
|
||||
partner_ids: [partnerId],
|
||||
});
|
||||
const def = new Deferred();
|
||||
mockService("action", {
|
||||
async doAction(action) {
|
||||
if (action?.res_model === "res.fake") {
|
||||
return super.doAction(...arguments);
|
||||
}
|
||||
asyncStep("do-action");
|
||||
expect(action.name).toBe("Log note");
|
||||
expect(action.context.default_subtype_xmlid).toBe("mail.mt_note");
|
||||
expect(action.context.default_partner_ids).toBeEmpty();
|
||||
def.resolve();
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(John Jane)");
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
|
||||
await click("button", { text: "Log note" });
|
||||
await click("button[title='Open Full Composer']");
|
||||
await def;
|
||||
await waitForSteps(["do-action"]);
|
||||
});
|
||||
|
||||
test("Check that a partner is created for new followers when sending a message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "John Jane", email: "john@jane.be" },
|
||||
{ name: "Peter Johnson", email: "peter@johnson.be" },
|
||||
]);
|
||||
const fakeId = pyEnv["res.fake"].create({
|
||||
email_cc: "john@test.be",
|
||||
partner_ids: [partnerId],
|
||||
});
|
||||
pyEnv["mail.followers"].create({
|
||||
partner_id: partnerId_2,
|
||||
email: "peter@johnson.be",
|
||||
is_active: true,
|
||||
res_id: fakeId,
|
||||
res_model: "res.fake",
|
||||
});
|
||||
registerArchs(archs);
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await contains(".o-mail-Followers-counter", { text: "1" });
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(John Jane)");
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
|
||||
// Ensure that partner `john@test.be` is created while sending the message (not before)
|
||||
const partners = pyEnv["res.partner"].search_read([["email", "=", "john@test.be"]]);
|
||||
expect(partners).toHaveLength(0);
|
||||
await insertText(".o-mail-Composer-input", "Dummy Message");
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o-mail-Followers-counter", { text: "1" });
|
||||
});
|
||||
|
||||
test("suggest recipient on 'Send message' composer", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "Peter Johnson",
|
||||
email: "peter@johnson.be",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({ email_cc: "john@test.be" });
|
||||
pyEnv["mail.followers"].create({
|
||||
partner_id: partnerId,
|
||||
email: "peter@johnson.be",
|
||||
is_active: true,
|
||||
res_id: fakeId,
|
||||
res_model: "res.fake",
|
||||
});
|
||||
registerArchs(archs);
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await contains(".o-mail-Followers-counter", { text: "1" });
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
|
||||
// Ensure that partner `john@test.be` is created before sending the message
|
||||
expect(pyEnv["res.partner"].search_read([["email", "=", "john@test.be"]])).toHaveLength(0);
|
||||
await insertText(".o-mail-Composer-input", "Dummy Message");
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await tick();
|
||||
expect(pyEnv["res.partner"].search_read([["email", "=", "john@test.be"]])).toHaveLength(1);
|
||||
await contains(".o-mail-Followers-counter", { text: "1" });
|
||||
});
|
||||
|
||||
test("suggested recipients should not be notified when posting an internal note", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "John Jane",
|
||||
email: "john@jane.be",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partnerId] });
|
||||
onRpcBefore("/mail/message/post", (args) => {
|
||||
asyncStep("message_post");
|
||||
expect(args.post_data.partner_ids).toBeEmpty();
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await click("button", { text: "Log note" });
|
||||
await insertText(".o-mail-Composer-input", "Dummy Message");
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o-mail-Message");
|
||||
await waitForSteps(["message_post"]);
|
||||
});
|
||||
|
||||
test("suggested recipients without name should show display_name instead", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partner1, partner2] = pyEnv["res.partner"].create([
|
||||
{ name: "Test Partner" },
|
||||
// Partner without name
|
||||
{ type: "invoice" },
|
||||
]);
|
||||
|
||||
pyEnv["res.partner"].write([partner2], { parent_id: partner1 });
|
||||
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partner2] });
|
||||
registerArchs(archs);
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await click("button", { text: "Send message" });
|
||||
await contains(".o-mail-RecipientsInput .o_tag_badge_text", { text: "Test Partner, Invoice" });
|
||||
});
|
||||
|
||||
test("update email for the partner on the fly", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "John Jane",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partnerId] });
|
||||
registerArchs(archs);
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await click("button", { text: "Send message" });
|
||||
await insertText(".o-mail-RecipientsInputTagsListPopover input", "john@jane.be");
|
||||
await click(".o-mail-RecipientsInputTagsListPopover .btn-primary");
|
||||
|
||||
await insertText(".o-mail-Composer-input", "Dummy Message");
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Followers-counter", { text: "0" });
|
||||
});
|
||||
|
||||
test("suggested recipients should not be added as follower when posting a message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "John Jane",
|
||||
email: "john@jane.be",
|
||||
});
|
||||
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partnerId] });
|
||||
registerArchs(archs);
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
await contains(".o-mail-Followers-counter", { text: "0" });
|
||||
await click("button", { text: "Send message" });
|
||||
await insertText(".o-mail-Composer-input", "Dummy Message");
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Followers-counter", { text: "0" });
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { defineMailModels, start } from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { getService } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Attachment model properties", async () => {
|
||||
await start();
|
||||
const attachment = getService("mail.store")["ir.attachment"].insert({
|
||||
id: 750,
|
||||
mimetype: "text/plain",
|
||||
name: "test.txt",
|
||||
});
|
||||
expect(attachment.isText).toBe(true);
|
||||
expect(attachment.isViewable).toBe(true);
|
||||
expect(attachment.mimetype).toBe("text/plain");
|
||||
expect(attachment.name).toBe("test.txt");
|
||||
expect(attachment.extension).toBe("txt");
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { Action } from "@mail/core/common/action";
|
||||
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("store is correctly set on actions", async () => {
|
||||
const storeSym = Symbol("STORE");
|
||||
const ownerSym = Symbol("COMPONENT");
|
||||
const action = new Action({
|
||||
owner: ownerSym,
|
||||
id: "test",
|
||||
definition: {},
|
||||
store: storeSym,
|
||||
});
|
||||
expect(action.store).toBe(storeSym);
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-dom";
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("following internal link from chatter does not open chat window", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Jeanne" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: `Created by <a href="#" data-oe-model="res.partner" data-oe-id="${pyEnv.user.partner_id}">Admin</a>`,
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o_last_breadcrumb_item", { text: "Jeanne" });
|
||||
await click("a", { text: "Admin" });
|
||||
await contains(".o_last_breadcrumb_item", { text: "Mitchell Admin" });
|
||||
// Assert 0 chat windows not sufficient because not enough time for potential chat window opening.
|
||||
// Let's open another chat window to give some time and assert only manually open chat window opens.
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click("button", { text: "New Message" });
|
||||
await insertText("input[placeholder='Search a conversation']", "abc");
|
||||
await click("a", { text: "Create Channel" });
|
||||
await contains(".o-mail-ChatWindow-header", { text: "abc" });
|
||||
await contains(".o-mail-ChatWindow", { count: 1 });
|
||||
});
|
||||
|
||||
test("message link shows error when the message is not known", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Alice" });
|
||||
const url = `${getOrigin()}/mail/message/999999`;
|
||||
pyEnv["mail.message"].create({
|
||||
body: `Check this out <a class="o_message_redirect" href="${url}" data-oe-model="mail.message" data-oe-id="999999">${url}</a>`,
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("a.o_message_redirect");
|
||||
await contains(".o_notification:contains(This conversation isn’t available.)");
|
||||
});
|
||||
|
||||
test("same-thread message link does not open the thread again but highlights the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [aliceId, lenaId] = pyEnv["res.partner"].create([{ name: "Alice" }, { name: "Lena" }]);
|
||||
const helloMessageId = pyEnv["mail.message"].create({
|
||||
body: "Hello",
|
||||
model: "res.partner",
|
||||
res_id: aliceId,
|
||||
});
|
||||
const heyMessageId = pyEnv["mail.message"].create({
|
||||
body: "Hey",
|
||||
model: "res.partner",
|
||||
res_id: lenaId,
|
||||
});
|
||||
const helloUrl = `${getOrigin()}/mail/message/${helloMessageId}`;
|
||||
pyEnv["mail.message"].create({
|
||||
body: `Check this out <a class="o_message_redirect" href="${helloUrl}" data-oe-model="mail.message" data-oe-id="${helloMessageId}">${helloUrl}</a>`,
|
||||
model: "res.partner",
|
||||
res_id: aliceId,
|
||||
});
|
||||
const heyUrl = `${getOrigin()}/mail/message/${heyMessageId}`;
|
||||
pyEnv["mail.message"].create({
|
||||
body: `Another thread <a class="o_message_redirect" href="${heyUrl}" data-oe-model="mail.message" data-oe-id="${heyMessageId}">${heyUrl}</a>`,
|
||||
model: "res.partner",
|
||||
res_id: aliceId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", aliceId);
|
||||
await click("a.o_message_redirect:contains(Alice)");
|
||||
await contains(".o-mail-Message.o-highlighted:contains(Hello)");
|
||||
await animationFrame(); // give enough time for the potential breadcrumb item to render
|
||||
await contains(".breadcrumb-item", { count: 0 });
|
||||
await click("a.o_message_redirect:contains(Lena)");
|
||||
await contains(".o-mail-Message.o-highlighted:contains(Hey)");
|
||||
await contains(".breadcrumb-item:contains(Alice)");
|
||||
});
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { Message } from "@mail/core/common/message_model";
|
||||
|
||||
import { defineMailModels, start } from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
|
||||
import {
|
||||
asyncStep,
|
||||
getService,
|
||||
patchWithCleanup,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
defineMailModels();
|
||||
|
||||
test("store.insert can delete record", async () => {
|
||||
await start();
|
||||
const store = getService("mail.store");
|
||||
store.insert({ "mail.message": [{ id: 1 }] });
|
||||
expect(store["mail.message"].get({ id: 1 })?.id).toBe(1);
|
||||
store.insert({ "mail.message": [{ id: 1, _DELETE: true }] });
|
||||
expect(store["mail.message"].get({ id: 1 })?.id).toBe(undefined);
|
||||
});
|
||||
|
||||
test("store.insert deletes record without creating it", async () => {
|
||||
patchWithCleanup(Message, {
|
||||
new() {
|
||||
const message = super.new(...arguments);
|
||||
asyncStep(`new-${message.id}`);
|
||||
return message;
|
||||
},
|
||||
});
|
||||
await start();
|
||||
const store = getService("mail.store");
|
||||
store.insert({ "mail.message": [{ id: 1, _DELETE: true }] });
|
||||
await waitForSteps([]);
|
||||
expect(store["mail.message"].get({ id: 1 })?.id).toBe(undefined);
|
||||
store.insert({ "mail.message": [{ id: 2 }] });
|
||||
await waitForSteps(["new-2"]);
|
||||
});
|
||||
|
||||
test("store.insert deletes record after relation created it", async () => {
|
||||
patchWithCleanup(Message, {
|
||||
new() {
|
||||
const message = super.new(...arguments);
|
||||
asyncStep(`new-${message.id}`);
|
||||
return message;
|
||||
},
|
||||
});
|
||||
await start();
|
||||
const store = getService("mail.store");
|
||||
store.insert({
|
||||
"mail.message": [{ id: 1, _DELETE: true }],
|
||||
// they key coverage of the test is to have the relation listed after the delete
|
||||
"mail.link.preview": [{ id: 1 }],
|
||||
"mail.message.link.preview": [{ id: 1, link_preview_id: 1, message_id: 1 }],
|
||||
});
|
||||
await waitForSteps(["new-1"]);
|
||||
expect(store["mail.message"].get({ id: 1 })?.id).toBe(undefined);
|
||||
});
|
||||
|
||||
test("store.insert different PY model having same JS model", async () => {
|
||||
await start();
|
||||
const store = getService("mail.store");
|
||||
const data = {
|
||||
"discuss.channel": [
|
||||
{ id: 1, name: "General" },
|
||||
{ id: 2, name: "Sales" },
|
||||
],
|
||||
"mail.thread": [
|
||||
{ id: 1, model: "discuss.channel" },
|
||||
{ id: 3, name: "R&D", model: "discuss.channel" },
|
||||
],
|
||||
};
|
||||
|
||||
store.insert(data);
|
||||
expect(store.Thread.records).toHaveLength(6); // 3 mailboxes + 3 channels
|
||||
expect(Boolean(store.Thread.get({ id: 1, model: "discuss.channel" }))).toBe(true);
|
||||
expect(Boolean(store.Thread.get({ id: 2, model: "discuss.channel" }))).toBe(true);
|
||||
expect(Boolean(store.Thread.get({ id: 3, model: "discuss.channel" }))).toBe(true);
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { defineMailModels, start } from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
import { deserializeDateTime, serializeDateTime } from "@web/core/l10n/dates";
|
||||
import { getService, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Message model properties", async () => {
|
||||
await start();
|
||||
const store = getService("mail.store");
|
||||
store.Store.insert({
|
||||
self_partner: { id: serverState.partnerId },
|
||||
});
|
||||
store.Thread.insert({
|
||||
id: serverState.partnerId,
|
||||
model: "res.partner",
|
||||
name: "general",
|
||||
});
|
||||
store["ir.attachment"].insert({
|
||||
id: 750,
|
||||
mimetype: "text/plain",
|
||||
name: "test.txt",
|
||||
});
|
||||
const message = store["mail.message"].insert({
|
||||
attachment_ids: 750,
|
||||
author_id: { id: 5, name: "Demo" },
|
||||
body: markup`<p>Test</p>`,
|
||||
date: deserializeDateTime("2019-05-05 10:00:00"),
|
||||
id: 4000,
|
||||
starred: true,
|
||||
model: "res.partner",
|
||||
thread: { id: serverState.partnerId, model: "res.partner" },
|
||||
res_id: serverState.partnerId,
|
||||
});
|
||||
expect(message.body?.toString()).toBe("<p>Test</p>");
|
||||
expect(serializeDateTime(message.date)).toBe("2019-05-05 10:00:00");
|
||||
expect(message.id).toBe(4000);
|
||||
expect(message.attachment_ids[0].name).toBe("test.txt");
|
||||
expect(message.thread.id).toBe(serverState.partnerId);
|
||||
expect(message.thread.name).toBe("general");
|
||||
expect(message.author_id.id).toBe(5);
|
||||
expect(message.author_id.name).toBe("Demo");
|
||||
});
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
import { waitNotifications } from "@bus/../tests/bus_test_helpers";
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
listenStoreFetch,
|
||||
openDiscuss,
|
||||
openFormView,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
waitStoreFetch,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { click as hootClick, press, queryFirst } from "@odoo/hoot-dom";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import { Command, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("keep new message separator when message is deleted", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const generalId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "message 0",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
author_id: serverState.partnerId,
|
||||
res_id: generalId,
|
||||
},
|
||||
{
|
||||
body: "message 1",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
author_id: serverState.partnerId,
|
||||
res_id: generalId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(generalId);
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
queryFirst(".o-mail-Composer-input").blur();
|
||||
await click("[title='Expand']", {
|
||||
parent: [".o-mail-Message", { text: "message 0" }],
|
||||
});
|
||||
await click(".o-dropdown-item:contains('Mark as Unread')");
|
||||
await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", { text: "message 0" });
|
||||
await click("[title='Expand']", {
|
||||
parent: [".o-mail-Message", { text: "message 0" }],
|
||||
});
|
||||
await click(".o-dropdown-item:contains('Delete')");
|
||||
await click(".modal button", { text: "Delete" });
|
||||
await contains(".o-mail-Message", { text: "message 0", count: 0 });
|
||||
await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", { text: "message 1" });
|
||||
});
|
||||
|
||||
test("new message separator is not shown if all messages are new", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const bobPartnerId = pyEnv["res.partner"].create({ name: "Bob" });
|
||||
for (let i = 0; i < 5; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: bobPartnerId,
|
||||
body: `message ${i}`,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 5 });
|
||||
await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New" });
|
||||
});
|
||||
|
||||
test("new message separator is shown after first mark as read, on receiving new message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const bobPartnerId = pyEnv["res.partner"].create({ name: "Bob" });
|
||||
const bobUserId = pyEnv["res.users"].create({ name: "Bob", partner_id: bobPartnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: bobPartnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: bobPartnerId,
|
||||
body: `Message 0`,
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { text: "Message 0" });
|
||||
await contains(".o-mail-Thread-newMessage", { count: 0, text: "New" });
|
||||
await withUser(bobUserId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: {
|
||||
body: "Message 1",
|
||||
message_type: "comment",
|
||||
subtype_xmlid: "mail.mt_comment",
|
||||
},
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", { text: "Message 1" });
|
||||
await contains(".o-mail-Thread-newMessage", { text: "New" });
|
||||
});
|
||||
|
||||
test("keep new message separator until user goes back to the thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: partnerId }),
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
],
|
||||
});
|
||||
const messageIds = pyEnv["mail.message"].create([
|
||||
{
|
||||
author_id: partnerId,
|
||||
body: "Message body 1",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
},
|
||||
{
|
||||
author_id: partnerId,
|
||||
body: "Message body 2",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
},
|
||||
]);
|
||||
// simulate that there is at least one read message in the channel
|
||||
const [memberId] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId], { new_message_separator: messageIds[0] + 1 });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Thread");
|
||||
await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", { text: "Message body 2" });
|
||||
await contains(".o-mail-Thread-newMessage:contains('New')");
|
||||
await hootClick(document.body); // Force "focusin" back on the textarea
|
||||
await hootClick(".o-mail-Composer-input");
|
||||
await waitNotifications([
|
||||
"mail.record/insert",
|
||||
(n) => n["discuss.channel.member"][0].new_message_separator,
|
||||
]);
|
||||
await hootClick(".o-mail-DiscussSidebar-item:contains(History)");
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "History" });
|
||||
await hootClick(".o-mail-DiscussSidebar-item:contains(test)");
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "test" });
|
||||
await contains(".o-mail-Message", { text: "Message body 2" });
|
||||
await contains(".o-mail-Thread-newMessage:contains('New')", { count: 0 });
|
||||
});
|
||||
|
||||
test("show new message separator on receiving new message when out of odoo focus", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" });
|
||||
const userId = pyEnv["res.users"].create({
|
||||
name: "Foreigner user",
|
||||
partner_id: partnerId,
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ message_unread_counter: 0, partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
name: "General",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
// simulate that there is at least one read message in the channel
|
||||
const [memberId] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId], { new_message_separator: messageId + 1 });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Thread");
|
||||
await contains(".o-mail-Thread-newMessage:contains('New')", { count: 0 });
|
||||
// simulate receiving a message
|
||||
await withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "hu", message_type: "comment", subtype_xmlid: "mail.mt_comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-Message", { text: "hu" });
|
||||
await contains(".o-mail-Thread-newMessage:contains('New')");
|
||||
await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", { text: "hu" });
|
||||
});
|
||||
|
||||
test("keep new message separator until current user sends a message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "hello");
|
||||
await triggerHotkey("Enter");
|
||||
await contains(".o-mail-Message", { text: "hello" });
|
||||
await click(".o-mail-Message [title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Mark as Unread')");
|
||||
await contains(".o-mail-Thread-newMessage:contains('New')");
|
||||
await insertText(".o-mail-Composer-input", "hey!");
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
await contains(".o-mail-Thread-newMessage:contains('New')", { count: 0 });
|
||||
});
|
||||
|
||||
test("keep new message separator when switching between chat window and discuss of same thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({ channel_type: "channel", name: "General" });
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click("button", { text: "General" });
|
||||
await insertText(".o-mail-Composer-input", "Very important message!");
|
||||
await triggerHotkey("Enter");
|
||||
await click(".o-mail-Message [title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Mark as Unread')");
|
||||
await contains(".o-mail-Thread-newMessage");
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Open in Discuss" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
|
||||
await contains(".o-mail-Thread-newMessage");
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await contains(".o-mail-ChatWindow-header", { text: "General" });
|
||||
await contains(".o-mail-Thread-newMessage");
|
||||
});
|
||||
|
||||
test("show new message separator when message is received in chat window", async () => {
|
||||
mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!)
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
unpin_dt: "2021-01-01 12:00:00",
|
||||
last_interest_dt: "2021-01-01 10:00:00",
|
||||
partner_id: serverState.partnerId,
|
||||
}),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const [memberId] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId], { new_message_separator: messageId + 1 });
|
||||
setupChatHub({ opened: [channelId] });
|
||||
await start();
|
||||
// simulate receiving a message
|
||||
withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "hu", message_type: "comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-ChatWindow");
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
await contains(".o-mail-Thread-newMessage:contains('New'):contains('New')");
|
||||
await contains(".o-mail-Thread-newMessage + .o-mail-Message", { text: "hu" });
|
||||
});
|
||||
|
||||
test("show new message separator when message is received while chat window is closed", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const userId = pyEnv["res.users"].create({
|
||||
name: "Foreigner user",
|
||||
partner_id: partnerId,
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
// simulate that there is at least one read message in the channel
|
||||
const [memberId] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId], { new_message_separator: messageId + 1 });
|
||||
setupChatHub({ opened: [channelId] });
|
||||
listenStoreFetch("init_messaging");
|
||||
await start();
|
||||
await waitStoreFetch("init_messaging");
|
||||
await click(".o-mail-ChatWindow-header [title*='Close Chat Window']");
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
// send after init_messaging because bus subscription is done after init_messaging
|
||||
// simulate receiving a message
|
||||
await withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "hu", message_type: "comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-ChatBubble");
|
||||
await contains(".o-mail-ChatBubble-counter", { text: "1" });
|
||||
await click(".o-mail-ChatBubble");
|
||||
await contains(".o-mail-Thread-newMessage:contains('New')");
|
||||
});
|
||||
|
||||
test("only show new message separator in its thread", async () => {
|
||||
// when a message acts as the reference for displaying new message separator,
|
||||
// this should applies only when vieweing the message in its thread.
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const demoPartnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const messageIds = pyEnv["mail.message"].create([
|
||||
{
|
||||
author_id: demoPartnerId,
|
||||
body: "Hello",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
},
|
||||
{
|
||||
author_id: demoPartnerId,
|
||||
body: "@Mitchell Admin",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
},
|
||||
]);
|
||||
// simulate that there is at least one read message in the channel
|
||||
const [memberId] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId], { new_message_separator: messageIds[0] + 1 });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", { text: "@Mitchell Admin" });
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "Inbox" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "Inbox" });
|
||||
await contains(".o-mail-Message", { text: "@Mitchell Admin" });
|
||||
await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", {
|
||||
count: 0,
|
||||
text: "@Mitchell Admin",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
contains,
|
||||
defineMailModels,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Command, getService, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("openChat: display notification for partner without user", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
await start();
|
||||
await getService("mail.store").openChat({ partnerId });
|
||||
await contains(".o_notification:has(.o_notification_bar.bg-info)", {
|
||||
text: "You can only chat with partners that have a dedicated user.",
|
||||
});
|
||||
});
|
||||
|
||||
test("openChat: display notification for wrong user", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].create({});
|
||||
await start();
|
||||
// userId not in the server data
|
||||
await getService("mail.store").openChat({ userId: 4242 });
|
||||
await contains(".o_notification:has(.o_notification_bar.bg-warning)", {
|
||||
text: "You can only chat with existing users.",
|
||||
});
|
||||
});
|
||||
|
||||
test("openChat: open new chat for user", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
await start();
|
||||
await contains(".o-mail-ChatHub");
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
getService("mail.store").openChat({ partnerId });
|
||||
await contains(".o-mail-ChatWindow");
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("openChat: open existing chat for user", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
setupChatHub({ opened: [channelId] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatWindow .o-mail-Composer-input:not(:focus)");
|
||||
getService("mail.store").openChat({ partnerId });
|
||||
await contains(".o-mail-ChatWindow .o-mail-Composer-input:focus");
|
||||
});
|
||||
1414
odoo-bringout-oca-ocb-mail/mail/static/tests/core/record.test.js
Normal file
1414
odoo-bringout-oca-ocb-mail/mail/static/tests/core/record.test.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,214 @@
|
|||
import { HIGHLIGHT_CLASS, searchHighlight } from "@mail/core/common/message_search_hook";
|
||||
import {
|
||||
SIZES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
import { serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Search highlight", async () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: markup`test odoo`,
|
||||
output: `test <span class="${HIGHLIGHT_CLASS}">odoo</span>`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: markup`<a href="https://www.odoo.com">https://www.odoo.com</a>`,
|
||||
output: `<a href="https://www.odoo.com">https://www.<span class="${HIGHLIGHT_CLASS}">odoo</span>.com</a>`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: '<a href="https://www.odoo.com">https://www.odoo.com</a>',
|
||||
output: `<a href="https://www.<span class="${HIGHLIGHT_CLASS}">odoo</span>.com">https://www.<span class="${HIGHLIGHT_CLASS}">odoo</span>.com</a>`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: markup`<a href="https://www.odoo.com">Odoo</a>`,
|
||||
output: `<a href="https://www.odoo.com"><span class="${HIGHLIGHT_CLASS}">Odoo</span></a>`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: markup`<a href="https://www.odoo.com">Odoo</a> Odoo is a free software`,
|
||||
output: `<a href="https://www.odoo.com"><span class="${HIGHLIGHT_CLASS}">Odoo</span></a> <span class="${HIGHLIGHT_CLASS}">Odoo</span> is a free software`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: markup`odoo is a free software`,
|
||||
output: `<span class="${HIGHLIGHT_CLASS}">odoo</span> is a free software`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: markup`software ODOO is a free`,
|
||||
output: `software <span class="${HIGHLIGHT_CLASS}">ODOO</span> is a free`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: markup`<ul>
|
||||
<li>Odoo</li>
|
||||
<li><a href="https://odoo.com">Odoo ERP</a> Best ERP</li>
|
||||
</ul>`,
|
||||
output: `<ul>
|
||||
<li><span class="${HIGHLIGHT_CLASS}">Odoo</span></li>
|
||||
<li><a href="https://odoo.com"><span class="${HIGHLIGHT_CLASS}">Odoo</span> ERP</a> Best ERP</li>
|
||||
</ul>`,
|
||||
searchTerm: "odoo",
|
||||
},
|
||||
{
|
||||
input: markup`test <strong>Odoo</strong> test`,
|
||||
output: `<span class="${HIGHLIGHT_CLASS}">test</span> <strong><span class="${HIGHLIGHT_CLASS}">Odoo</span></strong> <span class="${HIGHLIGHT_CLASS}">test</span>`,
|
||||
searchTerm: "odoo test",
|
||||
},
|
||||
{
|
||||
input: markup`test <br> test`,
|
||||
output: `<span class="${HIGHLIGHT_CLASS}">test</span> <br> <span class="${HIGHLIGHT_CLASS}">test</span>`,
|
||||
searchTerm: "odoo test",
|
||||
},
|
||||
{
|
||||
input: markup`<strong>test</strong> test`,
|
||||
output: `<strong><span class="${HIGHLIGHT_CLASS}">test</span></strong> <span class="${HIGHLIGHT_CLASS}">test</span>`,
|
||||
searchTerm: "test",
|
||||
},
|
||||
{
|
||||
input: markup`<strong>a</strong> test`,
|
||||
output: `<strong><span class="${HIGHLIGHT_CLASS}">a</span></strong> <span class="${HIGHLIGHT_CLASS}">test</span>`,
|
||||
searchTerm: "a test",
|
||||
},
|
||||
{
|
||||
input: markup`&amp;`,
|
||||
output: `<span class="${HIGHLIGHT_CLASS}">&amp;</span>`,
|
||||
searchTerm: "&",
|
||||
},
|
||||
{
|
||||
input: markup`&amp;`,
|
||||
output: `<span class="${HIGHLIGHT_CLASS}">&</span>amp;`,
|
||||
searchTerm: "&",
|
||||
},
|
||||
{
|
||||
input: markup`<strong>test</strong> hello`,
|
||||
output: `<strong><span class="${HIGHLIGHT_CLASS}">test</span></strong> <span class="${HIGHLIGHT_CLASS}">hello</span>`,
|
||||
searchTerm: "test hello",
|
||||
},
|
||||
{
|
||||
input: markup`<p><strong>test</strong> hello</p>`,
|
||||
output: `<p><strong><span class="${HIGHLIGHT_CLASS}">test</span></strong> <span class="${HIGHLIGHT_CLASS}">hello</span></p>`,
|
||||
searchTerm: "test hello",
|
||||
},
|
||||
];
|
||||
for (const { input, output, searchTerm } of testCases) {
|
||||
expect(searchHighlight(searchTerm, input).toString()).toBe(output);
|
||||
}
|
||||
});
|
||||
|
||||
test("Display highlighted search in chatter", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "empty");
|
||||
triggerHotkey("Enter");
|
||||
await contains(`.o-mail-SearchMessageResult .o-mail-Message span.${HIGHLIGHT_CLASS}`);
|
||||
});
|
||||
|
||||
test("Display multiple highlighted search in chatter", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not test empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "not empty");
|
||||
triggerHotkey("Enter");
|
||||
await contains(`.o-mail-SearchMessageResult .o-mail-Message span.${HIGHLIGHT_CLASS}`, {
|
||||
count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("Display highlighted search in Discuss", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "not empty",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await click("button[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "empty");
|
||||
triggerHotkey("Enter");
|
||||
await contains(`.o-mail-SearchMessagesPanel .o-mail-Message span.${HIGHLIGHT_CLASS}`);
|
||||
});
|
||||
|
||||
test("Display multiple highlighted search in Discuss", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "not prout empty",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await click("button[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "not empty");
|
||||
triggerHotkey("Enter");
|
||||
await contains(`.o-mail-SearchMessagesPanel .o-mail-Message span.${HIGHLIGHT_CLASS}`, {
|
||||
count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("Display highlighted with escaped character must ignore them", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<p><strong>test</strong> hello</p>",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "test hello");
|
||||
triggerHotkey("Enter");
|
||||
await contains(`.o-mail-SearchMessageResult .o-mail-Message span.${HIGHLIGHT_CLASS}`, {
|
||||
count: 2,
|
||||
});
|
||||
await contains(`.o-mail-Message-body`, { text: "<strong>test</strong> hello" });
|
||||
});
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
|
||||
import { inputFiles } from "@web/../tests/utils";
|
||||
import {
|
||||
asyncStep,
|
||||
getService,
|
||||
mockService,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Messages are received cross-tab", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const env1 = await start({ asTab: true });
|
||||
const env2 = await start({ asTab: true });
|
||||
await openDiscuss(channelId, { target: env1 });
|
||||
await openDiscuss(channelId, { target: env2 });
|
||||
await contains(`${env1.selector} .o-mail-Thread:contains('Welcome to #General!')`); // wait for loaded and focus in input
|
||||
await contains(`${env2.selector} .o-mail-Thread:contains('Welcome to #General!')`); // wait for loaded and focus in input
|
||||
await insertText(`${env1.selector} .o-mail-Composer-input`, "Hello World!");
|
||||
await press("Enter");
|
||||
await contains(`${env1.selector} .o-mail-Message-content`, { text: "Hello World!" });
|
||||
await contains(`${env2.selector} .o-mail-Message-content`, { text: "Hello World!" });
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("Thread rename", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
create_uid: serverState.userId,
|
||||
name: "General",
|
||||
});
|
||||
const env1 = await start({ asTab: true });
|
||||
const env2 = await start({ asTab: true });
|
||||
await openDiscuss(channelId, { target: env1 });
|
||||
await openDiscuss(channelId, { target: env2 });
|
||||
await insertText(`${env1.selector} .o-mail-DiscussContent-threadName:enabled`, "Sales", {
|
||||
replace: true,
|
||||
});
|
||||
triggerHotkey("Enter");
|
||||
await contains(`${env2.selector} .o-mail-DiscussContent-threadName[title='Sales']`);
|
||||
await contains(`${env2.selector} .o-mail-DiscussSidebarChannel`, { text: "Sales" });
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("Thread description update", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
create_uid: serverState.userId,
|
||||
name: "General",
|
||||
});
|
||||
const env1 = await start({ asTab: true });
|
||||
const env2 = await start({ asTab: true });
|
||||
await openDiscuss(channelId, { target: env1 });
|
||||
await openDiscuss(channelId, { target: env2 });
|
||||
await insertText(
|
||||
`${env1.selector} .o-mail-DiscussContent-threadDescription`,
|
||||
"The very best channel",
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
triggerHotkey("Enter");
|
||||
await contains(
|
||||
`${env2.selector} .o-mail-DiscussContent-threadDescription[title='The very best channel']`
|
||||
);
|
||||
});
|
||||
|
||||
test.skip("Channel subscription is renewed when channel is added from invite", async () => {
|
||||
const now = luxon.DateTime.now();
|
||||
mockDate(`${now.year}-${now.month}-${now.day} ${now.hour}:${now.minute}:${now.second}`);
|
||||
const pyEnv = await startServer();
|
||||
const [, channelId] = pyEnv["discuss.channel"].create([
|
||||
{ name: "R&D" },
|
||||
{ name: "Sales", channel_member_ids: [] },
|
||||
]);
|
||||
// Patch the date to consider those channels as already known by the server
|
||||
// when the client starts.
|
||||
const later = now.plus({ seconds: 10 });
|
||||
mockDate(
|
||||
`${later.year}-${later.month}-${later.day} ${later.hour}:${later.minute}:${later.second}`
|
||||
);
|
||||
await start();
|
||||
mockService("bus_service", {
|
||||
forceUpdateChannels() {
|
||||
asyncStep("update-channels");
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-DiscussSidebarChannel");
|
||||
getService("orm").call("discuss.channel", "add_members", [[channelId]], {
|
||||
partner_ids: [serverState.partnerId],
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarChannel", { count: 2 });
|
||||
await waitForSteps(["update-channels"]); // FIXME: sometimes 1 or 2 update-channels
|
||||
});
|
||||
|
||||
test("Adding attachments", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Hogwarts Legacy" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello world!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_type: "comment",
|
||||
});
|
||||
const env1 = await start({ asTab: true });
|
||||
const env2 = await start({ asTab: true });
|
||||
await openDiscuss(channelId, { target: env1 });
|
||||
await openDiscuss(channelId, { target: env2 });
|
||||
const file = new File(["file content"], "test.txt", { type: "text/plain" });
|
||||
await contains(`${env1.selector} .o-mail-Message:contains('Hello world!')`);
|
||||
await contains(`${env2.selector} .o-mail-Message:contains('Hello world!')`);
|
||||
await click(`${env1.selector} .o-mail-Message button[title='Edit']`);
|
||||
await click(`${env1.selector} .o-mail-Message .o-mail-Composer button[title='More Actions']`);
|
||||
await click(`${env1.selector} .o_popover button[name='upload-files']`);
|
||||
await inputFiles(`${env1.selector} .o-mail-Message .o-mail-Composer .o_input_file`, [file]);
|
||||
await contains(
|
||||
`${env1.selector} .o-mail-AttachmentContainer:not(.o-isUploading):contains(test.txt) .fa-check`
|
||||
);
|
||||
await click(`${env1.selector} .o-mail-Message .o-mail-Composer button[data-type='save']`);
|
||||
|
||||
await contains(
|
||||
`${env2.selector} .o-mail-AttachmentContainer:not(.o-isUploading):contains(test.txt)`
|
||||
);
|
||||
});
|
||||
|
||||
test("Remove attachment from message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const attachmentId = pyEnv["ir.attachment"].create({
|
||||
name: "test.txt",
|
||||
mimetype: "text/plain",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
attachment_ids: [attachmentId],
|
||||
body: "Hello World!",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const env1 = await start({ asTab: true });
|
||||
const env2 = await start({ asTab: true });
|
||||
await openDiscuss(channelId, { target: env1 });
|
||||
await openDiscuss(channelId, { target: env2 });
|
||||
await contains(`${env1.selector} .o-mail-AttachmentCard`, { text: "test.txt" });
|
||||
await click(`${env2.selector} .o-mail-Attachment-unlink`);
|
||||
await click(`${env2.selector} .modal-footer .btn`, { text: "Ok" });
|
||||
await contains(`${env1.selector} .o-mail-AttachmentCard`, { count: 0, text: "test.txt" });
|
||||
});
|
||||
|
||||
test("Message (hard) delete notification", async () => {
|
||||
// Note: This isn't a notification from when user click on "Delete message" action:
|
||||
// this happens when mail_message server record is effectively deleted (unlink)
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "Needaction message",
|
||||
model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
needaction: true,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_type: "inbox",
|
||||
notification_status: "sent",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await click("[title='Add Star']");
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { text: "1" }] });
|
||||
await contains("button", { text: "Starred messages", contains: [".badge", { text: "1" }] });
|
||||
const [partner] = pyEnv["res.partner"].read(serverState.partnerId);
|
||||
pyEnv["bus.bus"]._sendone(partner, "mail.message/delete", {
|
||||
message_ids: [messageId],
|
||||
});
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { count: 0 }] });
|
||||
await contains("button", { text: "Starred messages", contains: [".badge", { count: 0 }] });
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
start,
|
||||
startServer,
|
||||
openDiscuss,
|
||||
mockGetMedia,
|
||||
onlineTest,
|
||||
defineMailModels,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { PeerToPeer, UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer";
|
||||
|
||||
defineMailModels();
|
||||
|
||||
function connectionReady(p2p) {
|
||||
return new Promise((resolve) => {
|
||||
p2p.addEventListener("update", ({ detail }) => {
|
||||
if (
|
||||
detail.name === UPDATE_EVENT.CONNECTION_CHANGE &&
|
||||
detail.payload.state === "connected"
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function mockPeerToPeerCallEnvironment({ channelId, remoteSessionId }) {
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
const localUserP2P = env.services["discuss.p2p"];
|
||||
const remoteUserP2P = new PeerToPeer({
|
||||
notificationRoute: "/mail/rtc/session/notify_call_members",
|
||||
});
|
||||
remoteUserP2P.connect(remoteSessionId, channelId);
|
||||
|
||||
onRpc("/mail/rtc/session/notify_call_members", async (req) => {
|
||||
const {
|
||||
params: { peer_notifications },
|
||||
} = await req.json();
|
||||
for (const [sender, , message] of peer_notifications) {
|
||||
/**
|
||||
* This is a simplification, if more than 2 users we should check notification.target to know which user
|
||||
* should get the notification.
|
||||
*/
|
||||
if (sender === rtc.selfSession.id) {
|
||||
await remoteUserP2P.handleNotification(sender, message);
|
||||
} else {
|
||||
await localUserP2P.handleNotification(sender, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
const localUserConnected = connectionReady(localUserP2P);
|
||||
const remoteUserConnected = connectionReady(remoteUserP2P);
|
||||
return { localUserConnected, remoteUserConnected };
|
||||
}
|
||||
|
||||
onlineTest("Can join a call in p2p", async (assert) => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const remoteSessionId = pyEnv["discuss.channel.rtc.session"].create({
|
||||
channel_member_id: pyEnv["discuss.channel.member"].create({
|
||||
channel_id: channelId,
|
||||
partner_id: pyEnv["res.partner"].create({ name: "Remote" }),
|
||||
}),
|
||||
channel_id: channelId,
|
||||
});
|
||||
const { localUserConnected, remoteUserConnected } = await mockPeerToPeerCallEnvironment({
|
||||
channelId,
|
||||
remoteSessionId,
|
||||
});
|
||||
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Join Call']");
|
||||
await contains(".o-discuss-Call");
|
||||
await contains(".o-discuss-CallParticipantCard[title='Remote']");
|
||||
await Promise.all([localUserConnected, remoteUserConnected]);
|
||||
await contains("span[data-connection-state='connected']");
|
||||
});
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
mockGetMedia,
|
||||
mockPermissionsPrompt,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Starting a video call asks for permissions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Video Call']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Camera" });
|
||||
await contains(".o-discuss-CallActionList button[title='Stop camera']");
|
||||
});
|
||||
|
||||
test("Turning on the microphone asks for permissions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await click(".o-discuss-CallActionList button[title='Unmute']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.microphonePermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Microphone" });
|
||||
await contains(".o-discuss-CallActionList button[title='Mute']");
|
||||
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
});
|
||||
|
||||
test("Turning on the camera asks for permissions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await click(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Camera" });
|
||||
await contains(".o-discuss-CallActionList button[title='Stop camera']");
|
||||
});
|
||||
|
||||
test("Turn on both microphone and camera from permission dialog", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await click(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await contains(".modal[role='dialog']", { count: 1 });
|
||||
rtc.microphonePermission = "granted";
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use microphone and camera" });
|
||||
await contains(".o-discuss-CallActionList button[title='Stop camera']");
|
||||
await contains(".o-discuss-CallActionList button[title='Mute']");
|
||||
});
|
||||
|
||||
test("Combined mic+camera button only shown when both permissions not granted", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
mockGetMedia();
|
||||
mockPermissionsPrompt();
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await click(".o-discuss-CallActionList button[title='Turn camera on']");
|
||||
await contains(".modal-footer button", { count: 2 });
|
||||
await contains(".modal-footer button", { text: "Use microphone and camera" });
|
||||
await contains(".modal-footer button", { text: "Use Camera" });
|
||||
rtc.cameraPermission = "granted";
|
||||
await click(".modal-footer button", { text: "Use Camera" });
|
||||
await click(".o-discuss-CallActionList button[title='Unmute']");
|
||||
await contains(".modal-footer button");
|
||||
await contains(".modal-footer button", { text: "Use Microphone" });
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Call has Picture-in-picture feature", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("[title='Start Call']");
|
||||
await contains(".o-discuss-Call");
|
||||
await contains(".o-discuss-Call-layoutActions button[title='Picture in Picture']");
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
editInput,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test, expect } from "@odoo/hoot";
|
||||
import { advanceTime } from "@odoo/hoot-mock";
|
||||
import { asyncStep, patchWithCleanup, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Renders the call settings", async () => {
|
||||
patchWithCleanup(browser.navigator.mediaDevices, {
|
||||
enumerateDevices: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
deviceId: "mockAudioDeviceId",
|
||||
kind: "audioinput",
|
||||
label: "mockAudioDeviceLabel",
|
||||
},
|
||||
{
|
||||
deviceId: "mockVideoDeviceId",
|
||||
kind: "videoinput",
|
||||
label: "mockVideoDeviceLabel",
|
||||
},
|
||||
]),
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const env = await start();
|
||||
const rtc = env.services["discuss.rtc"];
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await contains(".o-discuss-CallSettings");
|
||||
await contains("label[aria-label='Camera']");
|
||||
await contains("label[aria-label='Microphone']");
|
||||
await contains("label[aria-label='Audio Output']");
|
||||
await contains("option", { textContent: "Permission Needed", count: 3 });
|
||||
rtc.microphonePermission = "granted";
|
||||
await contains("option[value=mockAudioDeviceId]");
|
||||
rtc.cameraPermission = "granted";
|
||||
await contains("option[value=mockVideoDeviceId]");
|
||||
await contains("button", { text: "Voice Detection" });
|
||||
await contains("button", { text: "Push to Talk" });
|
||||
await contains("span", { text: "Voice detection sensitivity" });
|
||||
await contains("button", { text: "Test" });
|
||||
await contains("label", { text: "Show video participants only" });
|
||||
await contains("label", { text: "Blur video background" });
|
||||
});
|
||||
|
||||
test("activate push to talk", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Push to Talk" });
|
||||
await contains("i[aria-label='Register new key']");
|
||||
await contains("label", { text: "Delay after releasing push-to-talk" });
|
||||
await contains("label", { text: "Voice detection sensitivity", count: 0 });
|
||||
});
|
||||
|
||||
test("activate blur", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("input[title='Blur video background']");
|
||||
await contains("label", { text: "Blur video background" });
|
||||
await contains("label", { text: "Edge blur intensity" });
|
||||
});
|
||||
|
||||
test("local storage for call settings", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
localStorage.setItem("mail_user_setting_background_blur_amount", "3");
|
||||
localStorage.setItem("mail_user_setting_edge_blur_amount", "5");
|
||||
localStorage.setItem("mail_user_setting_show_only_video", "true");
|
||||
localStorage.setItem("mail_user_setting_use_blur", "true");
|
||||
patchWithCleanup(localStorage, {
|
||||
setItem(key, value) {
|
||||
if (key.startsWith("mail_user_setting")) {
|
||||
asyncStep(`${key}: ${value}`);
|
||||
}
|
||||
return super.setItem(key, value);
|
||||
},
|
||||
});
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// testing load from local storage
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await contains("input[title='Show video participants only']:checked");
|
||||
await contains("input[title='Blur video background']:checked");
|
||||
await contains("label[title='Background blur intensity']", { text: "15%" });
|
||||
await contains("label[title='Edge blur intensity']", { text: "25%" });
|
||||
|
||||
// testing save to local storage
|
||||
await click("input[title='Show video participants only']");
|
||||
await waitForSteps(["mail_user_setting_show_only_video: false"]);
|
||||
await click("input[title='Blur video background']");
|
||||
expect(localStorage.getItem("mail_user_setting_use_blur")).toBe(null);
|
||||
await editInput(document.body, ".o-Discuss-CallSettings-thresholdInput", 0.3);
|
||||
await advanceTime(2000); // threshold setting debounce timer
|
||||
await waitForSteps(["mail_user_setting_voice_threshold: 0.3"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
import { describe, expect } from "@odoo/hoot";
|
||||
import { advanceTime } from "@odoo/hoot-mock";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { asyncStep, onRpc, mountWebClient, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
import { defineMailModels, mockGetMedia, onlineTest } from "@mail/../tests/mail_test_helpers";
|
||||
import { PeerToPeer, STREAM_TYPE, UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
class Network {
|
||||
_peerToPeerInstances = new Map();
|
||||
_notificationRoute;
|
||||
constructor(route) {
|
||||
this._notificationRoute = route || "/any/mock/notification";
|
||||
onRpc(this._notificationRoute, async (req) => {
|
||||
const {
|
||||
params: { peer_notifications },
|
||||
} = await req.json();
|
||||
for (const notification of peer_notifications) {
|
||||
const [sender_session_id, target_session_ids, content] = notification;
|
||||
for (const id of target_session_ids) {
|
||||
const p2p = this._peerToPeerInstances.get(id);
|
||||
p2p.handleNotification(sender_session_id, content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param id
|
||||
* @return {{id, p2p: PeerToPeer}}
|
||||
*/
|
||||
register(id) {
|
||||
const p2p = new PeerToPeer({ notificationRoute: this._notificationRoute });
|
||||
this._peerToPeerInstances.set(id, p2p);
|
||||
return { id, p2p };
|
||||
}
|
||||
close() {
|
||||
for (const p2p of this._peerToPeerInstances.values()) {
|
||||
p2p.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onlineTest("basic peer to peer connection", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 1;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.CONNECTION_CHANGE && payload.state === "connected") {
|
||||
asyncStep(payload.state);
|
||||
}
|
||||
});
|
||||
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
await user1.p2p.addPeer(user2.id);
|
||||
await waitForSteps(["connected"]);
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("mesh peer to peer connections", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 2;
|
||||
const network = new Network();
|
||||
const userCount = 10;
|
||||
const users = Array.from({ length: userCount }, (_, i) => network.register(i));
|
||||
const promises = [];
|
||||
for (const user of users) {
|
||||
user.p2p.connect(user.id, channelId);
|
||||
for (let i = 0; i < user.id; i++) {
|
||||
promises.push(user.p2p.addPeer(i));
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
let connectionsCount = 0;
|
||||
for (const user of users) {
|
||||
connectionsCount += user.p2p.peers.size;
|
||||
}
|
||||
expect(connectionsCount).toBe(userCount * (userCount - 1));
|
||||
connectionsCount = 0;
|
||||
network.close();
|
||||
for (const user of users) {
|
||||
connectionsCount += user.p2p.peers.size;
|
||||
}
|
||||
expect(connectionsCount).toBe(0);
|
||||
});
|
||||
|
||||
onlineTest("connection recovery", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 1;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.remoteStates = new Map();
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.CONNECTION_CHANGE && payload.state === "connected") {
|
||||
asyncStep(payload.state);
|
||||
}
|
||||
});
|
||||
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
user1.p2p.addPeer(user2.id);
|
||||
// only connecting user2 after user1 has called addPeer so that user2 ignores notifications
|
||||
// from user1, which simulates a connection drop that should be recovered.
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
const openPromise = new Promise((resolve) => {
|
||||
user1.p2p.peers.get(2).dataChannel.onopen = resolve;
|
||||
});
|
||||
advanceTime(5_000); // recovery timeout
|
||||
await openPromise;
|
||||
await waitForSteps(["connected"]);
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("can broadcast a stream and control download", async () => {
|
||||
mockGetMedia();
|
||||
await mountWebClient();
|
||||
const channelId = 3;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.remoteMedia = new Map();
|
||||
const trackPromise = new Promise((resolve) => {
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.TRACK) {
|
||||
user2.remoteMedia.set(payload.sessionId, {
|
||||
[payload.type]: {
|
||||
track: payload.track,
|
||||
active: payload.active,
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
await user1.p2p.addPeer(user2.id);
|
||||
const videoStream = await browser.navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
});
|
||||
const videoTrack = videoStream.getVideoTracks()[0];
|
||||
await user1.p2p.updateUpload(STREAM_TYPE.CAMERA, videoTrack);
|
||||
await trackPromise;
|
||||
const user2RemoteMedia = user2.remoteMedia.get(user1.id);
|
||||
const user2CameraTransceiver = user2.p2p.peers.get(user1.id).getTransceiver(STREAM_TYPE.CAMERA);
|
||||
expect(user2CameraTransceiver.direction).toBe("recvonly");
|
||||
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].track.kind).toBe("video");
|
||||
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].active).toBe(true);
|
||||
user2.p2p.updateDownload(user1.id, { camera: false });
|
||||
expect(user2CameraTransceiver.direction).toBe("inactive");
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("can broadcast arbitrary messages (dataChannel)", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 4;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
await user1.p2p.addPeer(user2.id);
|
||||
user1.inbox = [];
|
||||
const pongPromise = new Promise((resolve) => {
|
||||
user1.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.BROADCAST) {
|
||||
user1.inbox.push(payload);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
user2.inbox = [];
|
||||
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
||||
if (name === UPDATE_EVENT.BROADCAST && payload.message === "ping") {
|
||||
user2.inbox.push(payload);
|
||||
user2.p2p.broadcast("pong");
|
||||
}
|
||||
});
|
||||
user1.p2p.broadcast("ping");
|
||||
await pongPromise;
|
||||
expect(user2.inbox[0].senderId).toBe(user1.id);
|
||||
expect(user2.inbox[0].message).toBe("ping");
|
||||
expect(user1.inbox[0].senderId).toBe(user2.id);
|
||||
expect(user1.inbox[0].message).toBe("pong");
|
||||
network.close();
|
||||
});
|
||||
|
||||
onlineTest("can reject arbitrary offers", async () => {
|
||||
await mountWebClient();
|
||||
const channelId = 1;
|
||||
const network = new Network();
|
||||
const user1 = network.register(1);
|
||||
const user2 = network.register(2);
|
||||
user2.p2p.connect(user2.id, channelId);
|
||||
user1.p2p.connect(user1.id, channelId);
|
||||
user2.p2p._emitLog = (id, message) => {
|
||||
if (message === "offer rejected") {
|
||||
asyncStep("offer rejected");
|
||||
}
|
||||
};
|
||||
user2.p2p.acceptOffer = (id, sequence) => id !== user1.id || sequence > 20;
|
||||
user1.p2p.addPeer(user2.id, { sequence: 19 });
|
||||
await waitForSteps(["offer rejected"]);
|
||||
network.close();
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
mockGetMedia,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { pttExtensionServiceInternal } from "@mail/discuss/call/common/ptt_extension_service";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("display banner when ptt extension is not enabled", async () => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
patchWithCleanup(pttExtensionServiceInternal, {
|
||||
onAnswerIsEnabled(pttService) {
|
||||
pttService.isEnabled = false;
|
||||
},
|
||||
});
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Push to Talk" });
|
||||
await click("[title*='Close Chat Window']");
|
||||
await click("button[title='New Meeting']");
|
||||
await click("button[title='Close panel']"); // invitation panel automatically open
|
||||
await contains(".o-discuss-PttAdBanner");
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Voice Detection" });
|
||||
await click("[title*='Close Chat Window']");
|
||||
await contains(".o-discuss-PttAdBanner", { count: 0 });
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
mockGetMedia,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { pttExtensionServiceInternal } from "@mail/discuss/call/common/ptt_extension_service";
|
||||
import { PTT_RELEASE_DURATION } from "@mail/discuss/call/common/rtc_service";
|
||||
import { advanceTime, freezeTime, keyDown, mockTouch, mockUserAgent, test } from "@odoo/hoot";
|
||||
import { patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
defineMailModels();
|
||||
|
||||
test.tags("desktop");
|
||||
test("no auto-call on joining chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await insertText("input[placeholder='Search a conversation']", "mario");
|
||||
await contains(".o_command_name", { count: 3 });
|
||||
await click(".o_command_name", { text: "Mario" });
|
||||
await contains(".o-mail-DiscussSidebar-item", { text: "Mario" });
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await contains(".o-discuss-Call", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("no auto-call on joining group chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "Mario" },
|
||||
{ name: "Luigi" },
|
||||
]);
|
||||
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await click("a", { text: "Create Chat" });
|
||||
await click("li", { text: "Mario" });
|
||||
await click("li", { text: "Luigi" });
|
||||
await click("button", { text: "Create Group Chat" });
|
||||
await contains(".o-mail-DiscussSidebar-item:contains('Mario, and Luigi')");
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await contains(".o-discuss-Call", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("show Push-to-Talk button on mobile", async () => {
|
||||
mockGetMedia();
|
||||
mockTouch(true);
|
||||
mockUserAgent("android");
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
patchWithCleanup(pttExtensionServiceInternal, {
|
||||
onAnswerIsEnabled(pttService) {
|
||||
pttService.isEnabled = false;
|
||||
},
|
||||
});
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-ChatWindow-moreActions", { text: "General" });
|
||||
await click(".o-dropdown-item:text('Start Call')");
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await click("button", { text: "Push to Talk" });
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Call Settings" });
|
||||
await contains("button", { text: "Push to talk" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Can push-to-talk", async () => {
|
||||
mockGetMedia();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["res.users.settings"].create({
|
||||
use_push_to_talk: true,
|
||||
user_id: serverState.userId,
|
||||
push_to_talk_key: "...f",
|
||||
});
|
||||
patchWithCleanup(pttExtensionServiceInternal, {
|
||||
onAnswerIsEnabled(pttService) {
|
||||
pttService.isEnabled = false;
|
||||
},
|
||||
});
|
||||
freezeTime();
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(1000);
|
||||
await click("[title='Start Call']");
|
||||
await advanceTime(1000);
|
||||
await contains(".o-discuss-Call");
|
||||
await click(".o-discuss-Call");
|
||||
await advanceTime(1000);
|
||||
await keyDown("f");
|
||||
await advanceTime(PTT_RELEASE_DURATION);
|
||||
await contains(".o-discuss-CallParticipantCard .o-isTalking");
|
||||
// switching tab while PTT key still pressed then released on other tab should eventually release PTT
|
||||
browser.dispatchEvent(new Event("blur"));
|
||||
await advanceTime(PTT_RELEASE_DURATION + 1000);
|
||||
await contains(".o-discuss-CallParticipantCard:not(:has(.o-isTalking))");
|
||||
await click(".o-discuss-Call");
|
||||
await advanceTime(1000);
|
||||
await keyDown("f");
|
||||
await advanceTime(PTT_RELEASE_DURATION);
|
||||
await contains(".o-discuss-CallParticipantCard .o-isTalking");
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Empty attachment panel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = await pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Attachments']");
|
||||
await contains(".o-mail-ActionPanel", {
|
||||
text: "This channel doesn't have any attachments.",
|
||||
});
|
||||
});
|
||||
|
||||
test("Attachment panel sort by date", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["ir.attachment"].create([
|
||||
{
|
||||
res_id: channelId,
|
||||
res_model: "discuss.channel",
|
||||
name: "file1.pdf",
|
||||
create_date: "2023-08-20 10:00:00",
|
||||
},
|
||||
{
|
||||
res_id: channelId,
|
||||
res_model: "discuss.channel",
|
||||
name: "file2.pdf",
|
||||
create_date: "2023-09-21 10:00:00",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Attachments']");
|
||||
await contains(".o-mail-AttachmentList", {
|
||||
text: "file2.pdf",
|
||||
after: [".o-mail-DateSection", { text: "September, 2023" }],
|
||||
before: [".o-mail-DateSection", { text: "August, 2023" }],
|
||||
});
|
||||
await contains(".o-mail-AttachmentList", {
|
||||
text: "file1.pdf",
|
||||
after: [".o-mail-DateSection", { text: "August, 2023" }],
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import { waitForChannels } from "@bus/../tests/bus_test_helpers";
|
||||
import { onWebsocketEvent } from "@bus/../tests/mock_websocket";
|
||||
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, edit, expect, mockDate, press, test } from "@odoo/hoot";
|
||||
|
||||
import { Command } from "@web/../tests/web_test_helpers";
|
||||
|
||||
defineMailModels();
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("bus subscription updated when joining/leaving thread as non member", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const johnUser = pyEnv["res.users"].create({ name: "John" });
|
||||
const johnPartner = pyEnv["res.partner"].create({ name: "John", user_ids: [johnUser] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [Command.create({ partner_id: johnPartner })],
|
||||
name: "General",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await waitForChannels([`discuss.channel_${channelId}`]);
|
||||
await click("[title='Channel Actions']");
|
||||
await click(".o-dropdown-item:contains('Leave Channel')");
|
||||
await click("button", { text: "Leave Conversation" });
|
||||
await waitForChannels([`discuss.channel_${channelId}`], { operation: "delete" });
|
||||
});
|
||||
|
||||
test("bus subscription updated when opening/closing chat window as a non member", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [],
|
||||
name: "Sales",
|
||||
});
|
||||
setupChatHub({ opened: [channelId] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatWindow", { text: "Sales" });
|
||||
await waitForChannels([`discuss.channel_${channelId}`]);
|
||||
await click("[title*='Close Chat Window']", {
|
||||
parent: [".o-mail-ChatWindow", { text: "Sales" }],
|
||||
});
|
||||
await contains(".o-mail-ChatWindow", { count: 0, text: "Sales" });
|
||||
await waitForChannels([`discuss.channel_${channelId}`], { operation: "delete" });
|
||||
await press(["control", "k"]);
|
||||
await click(".o_command_palette_search input");
|
||||
await edit("@");
|
||||
await click(".o-mail-DiscussCommand", { text: "Sales" });
|
||||
await waitForChannels([`discuss.channel_${channelId}`]);
|
||||
});
|
||||
|
||||
test("bus subscription updated when joining locally pinned thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [],
|
||||
name: "General",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await waitForChannels([`discuss.channel_${channelId}`]);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click("[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", {
|
||||
text: "Mitchell Admin",
|
||||
});
|
||||
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
|
||||
await waitForChannels([`discuss.channel_${channelId}`], { operation: "delete" });
|
||||
});
|
||||
|
||||
test("bus subscription is refreshed when channel is joined", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create([{ name: "General" }, { name: "Sales" }]);
|
||||
onWebsocketEvent("subscribe", () => expect.step("subscribe"));
|
||||
const later = luxon.DateTime.now().plus({ seconds: 2 });
|
||||
mockDate(
|
||||
`${later.year}-${later.month}-${later.day} ${later.hour}:${later.minute}:${later.second}`
|
||||
);
|
||||
await start();
|
||||
await expect.waitForSteps(["subscribe"]);
|
||||
await openDiscuss();
|
||||
await expect.waitForSteps([]);
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await insertText("input[placeholder='Search a conversation']", "new channel");
|
||||
await expect.waitForSteps(["subscribe"]);
|
||||
});
|
||||
|
||||
test("bus subscription is refreshed when channel is left", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({ name: "General" });
|
||||
onWebsocketEvent("subscribe", () => expect.step("subscribe"));
|
||||
const later = luxon.DateTime.now().plus({ seconds: 2 });
|
||||
mockDate(
|
||||
`${later.year}-${later.month}-${later.day} ${later.hour}:${later.minute}:${later.second}`
|
||||
);
|
||||
await start();
|
||||
await expect.waitForSteps(["subscribe"]);
|
||||
await openDiscuss();
|
||||
await expect.waitForSteps([]);
|
||||
await click("[title='Channel Actions']");
|
||||
await click(".o-dropdown-item:contains('Leave Channel')");
|
||||
await expect.waitForSteps(["subscribe"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
mockPermissionsPrompt,
|
||||
openDiscuss,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-dom";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import { Command, getService, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("should display the channel invitation form after clicking on the invite button of a chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Invite People']");
|
||||
await contains(".o-discuss-ChannelInvitation");
|
||||
});
|
||||
|
||||
test("can invite users in channel from chat window", async () => {
|
||||
mockDate("2025-01-01 12:00:00", +1);
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_type: "channel",
|
||||
});
|
||||
setupChatHub({ opened: [channelId] });
|
||||
await start();
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains("[title='Open Actions Menu']");
|
||||
await click("[title='Open Actions Menu']");
|
||||
await click(".o-dropdown-item", { text: "Invite People" });
|
||||
await contains(".o-discuss-ChannelInvitation");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner" });
|
||||
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
|
||||
await contains(".o-discuss-ChannelInvitation", { count: 0 });
|
||||
await contains(".o-mail-Thread .o-mail-NotificationMessage", {
|
||||
text: "Mitchell Admin invited TestPartner to the channel1:00 PM",
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to search for a new user to invite from an existing chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
const partnerId_2 = pyEnv["res.partner"].create({
|
||||
email: "testpartner2@odoo.com",
|
||||
name: "TestPartner2",
|
||||
});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId_1 });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId_2 });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Invite People']");
|
||||
await insertText(".o-discuss-ChannelInvitation-search", "TestPartner2");
|
||||
await contains(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner2" });
|
||||
});
|
||||
|
||||
test("Invitation form should display channel group restriction", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const groupId = pyEnv["res.groups"].create({
|
||||
name: "testGroup",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_type: "channel",
|
||||
group_public_id: groupId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Invite People']");
|
||||
await contains(".o-discuss-ChannelInvitation div", {
|
||||
text: 'Access restricted to group "testGroup"',
|
||||
after: ["button .fa.fa-copy"],
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to create a new group chat from an existing chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
const partnerId_2 = pyEnv["res.partner"].create({
|
||||
email: "testpartner2@odoo.com",
|
||||
name: "TestPartner2",
|
||||
});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId_1 });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId_2 });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-DiscussContent-header button[title='Invite People']");
|
||||
await contains(".o-discuss-ChannelInvitation");
|
||||
await insertText(".o-discuss-ChannelInvitation-search", "TestPartner2");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner2" });
|
||||
await click("button[title='Create Group Chat']:enabled");
|
||||
await contains(".o-discuss-ChannelInvitation", { count: 0 });
|
||||
await contains(".o-mail-DiscussSidebarChannel", {
|
||||
text: "Mitchell Admin, TestPartner, and TestPartner2",
|
||||
});
|
||||
});
|
||||
|
||||
test("unnamed group chat should display correct name just after being invited", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
email: "jane@example.com",
|
||||
name: "Jane",
|
||||
});
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const [, channelId] = pyEnv["discuss.channel"].create([
|
||||
{ name: "General" },
|
||||
{
|
||||
channel_member_ids: [Command.create({ partner_id: partnerId })],
|
||||
channel_type: "group",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-DiscussSidebarChannel", { text: "General" });
|
||||
await contains(".o-mail-DiscussSidebarChannel", { count: 0, text: "Jane and Mitchell Admin" });
|
||||
const currentPartnerId = serverState.partnerId;
|
||||
await withUser(userId, async () => {
|
||||
await getService("orm").call("discuss.channel", "add_members", [[channelId]], {
|
||||
partner_ids: [currentPartnerId],
|
||||
});
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarChannel", { text: "Jane and Mitchell Admin" });
|
||||
await contains(".o_notification", {
|
||||
text: "You have been invited to #Jane and Mitchell Admin",
|
||||
});
|
||||
});
|
||||
|
||||
test("invite user to self chat opens DM chat with user", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const guestId = pyEnv["mail.guest"].create({ name: "TestGuest" });
|
||||
const partnerId_1 = pyEnv["res.partner"].create({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
pyEnv["res.users"].create({ partner_id: partnerId_1 });
|
||||
const [selfChatId] = pyEnv["discuss.channel"].create([
|
||||
{
|
||||
channel_member_ids: [Command.create({ partner_id: serverState.partnerId })],
|
||||
channel_type: "chat",
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
],
|
||||
channel_type: "group",
|
||||
},
|
||||
{
|
||||
// group chat with guest as correspondent for coverage of no crash
|
||||
channel_member_ids: [
|
||||
Command.create({ guest_id: guestId }),
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
],
|
||||
channel_type: "group",
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(selfChatId);
|
||||
await contains(".o-mail-DiscussSidebarChannel", { text: "Mitchell Admin" }); // self-chat
|
||||
await contains(".o-mail-DiscussSidebarChannel", { text: "TestPartner and Mitchell Admin" });
|
||||
await contains(".o-mail-DiscussSidebarChannel", { text: "TestGuest and Mitchell Admin" });
|
||||
await contains(".o-mail-DiscussSidebarChannel", { text: "TestPartner" });
|
||||
await click(".o-mail-DiscussContent-header button[title='Invite People']");
|
||||
await insertText(".o-discuss-ChannelInvitation-search", "TestPartner");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner" });
|
||||
await click("button:contains('Go to Conversation'):enabled");
|
||||
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "TestPartner" });
|
||||
});
|
||||
|
||||
test("Invite sidebar action has the correct title for group chats", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Chat Actions']");
|
||||
await click(".o-dropdown-item", { text: "Invite People" });
|
||||
await contains(".modal-title", { text: "Mitchell Admin and Demo" });
|
||||
});
|
||||
|
||||
test("Active dialog retains focus over invite input", async () => {
|
||||
await startServer();
|
||||
mockPermissionsPrompt();
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("button[title='New Meeting']");
|
||||
await animationFrame();
|
||||
await contains(".o-discuss-ChannelInvitation");
|
||||
await contains("button:focus", { text: "Use Camera" });
|
||||
});
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Command, getService, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("there should be a button to show member list in the thread view topbar initially", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains("[title='Members']");
|
||||
});
|
||||
|
||||
test("should show member list when clicking on member list button in thread view topbar", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // open by default
|
||||
await click("[title='Members']");
|
||||
await contains(".o-discuss-ChannelMemberList", { count: 0 });
|
||||
await click("[title='Members']");
|
||||
await contains(".o-discuss-ChannelMemberList");
|
||||
});
|
||||
|
||||
test("should have correct members in member list", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMember", { count: 2 });
|
||||
await contains(".o-discuss-ChannelMember", { text: serverState.partnerName });
|
||||
await contains(".o-discuss-ChannelMember", { text: "Demo" });
|
||||
});
|
||||
|
||||
test("members should be correctly categorised into online/offline", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [onlinePartnerId, idlePartnerId] = pyEnv["res.partner"].create([
|
||||
{ name: "Online Partner", im_status: "online" },
|
||||
{ name: "Idle Partner", im_status: "away" },
|
||||
]);
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "im_partner" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChanel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: onlinePartnerId }),
|
||||
Command.create({ partner_id: idlePartnerId }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList h6", { text: "Online - 2" });
|
||||
await contains(".o-discuss-ChannelMemberList h6", { text: "Offline - 1" });
|
||||
});
|
||||
|
||||
test("chat with member should be opened after clicking on channel member", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-discuss-ChannelMember.cursor-pointer", { text: "Demo" });
|
||||
await contains(".o_avatar_card .o_card_user_infos", { text: "Demo" });
|
||||
await click(".o_avatar_card button", { text: "Send message" });
|
||||
await contains(".o-mail-AutoresizeInput[title='Demo']");
|
||||
});
|
||||
|
||||
test("should show a button to load more members if they are not all loaded", async () => {
|
||||
// Test assumes at most 100 members are loaded at once.
|
||||
const pyEnv = await startServer();
|
||||
const channel_member_ids = [];
|
||||
for (let i = 0; i < 101; i++) {
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "name" + i });
|
||||
channel_member_ids.push(Command.create({ partner_id: partnerId }));
|
||||
}
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
pyEnv["discuss.channel"].write([channelId], { channel_member_ids });
|
||||
await contains(
|
||||
".o-mail-ActionPanel:has(.o-mail-ActionPanel-header:contains('Members')) button",
|
||||
{ text: "Load more" }
|
||||
);
|
||||
});
|
||||
|
||||
test("Load more button should load more members", async () => {
|
||||
// Test assumes at most 100 members are loaded at once.
|
||||
const pyEnv = await startServer();
|
||||
const channel_member_ids = [];
|
||||
for (let i = 0; i < 101; i++) {
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "name" + i });
|
||||
channel_member_ids.push(Command.create({ partner_id: partnerId }));
|
||||
}
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "TestChannel",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
pyEnv["discuss.channel"].write([channelId], { channel_member_ids });
|
||||
await click(
|
||||
".o-mail-ActionPanel:has(.o-mail-ActionPanel-header:contains('Members')) [title='Load more']"
|
||||
);
|
||||
await contains(".o-discuss-ChannelMember", { count: 102 });
|
||||
});
|
||||
|
||||
test("Channel member count update after user joined", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const userId = pyEnv["res.users"].create({ name: "Harry" });
|
||||
pyEnv["res.partner"].create({ name: "Harry", user_ids: [userId] });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await contains(".o-discuss-ChannelMemberList h6", { text: "Offline - 1" });
|
||||
await click("[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "Harry" });
|
||||
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
|
||||
await contains(".o-discuss-ChannelInvitation", { count: 0 });
|
||||
await click("[title='Members']");
|
||||
await contains(".o-discuss-ChannelMemberList h6", { text: "Offline - 2" });
|
||||
});
|
||||
|
||||
test("Channel member count update after user left", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Dobby" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Dobby", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMember", { count: 2 });
|
||||
await withUser(userId, () =>
|
||||
getService("orm").call("discuss.channel", "action_unfollow", [channelId])
|
||||
);
|
||||
await contains(".o-discuss-ChannelMember", { count: 1 });
|
||||
});
|
||||
|
||||
test("Members are partitioned by online/offline", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [userId_1, userId_2] = pyEnv["res.users"].create([{ name: "Dobby" }, { name: "John" }]);
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{
|
||||
name: "Dobby",
|
||||
user_ids: [userId_1],
|
||||
im_status: "offline",
|
||||
},
|
||||
{
|
||||
name: "John",
|
||||
user_ids: [userId_2],
|
||||
im_status: "online",
|
||||
},
|
||||
]);
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
],
|
||||
});
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "online" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMember", { count: 3 });
|
||||
await contains("h6", { text: "Online - 2" });
|
||||
await contains("h6", { text: "Offline - 1" });
|
||||
await contains(".o-discuss-ChannelMember", {
|
||||
text: "John",
|
||||
after: ["h6", { text: "Online - 2" }],
|
||||
before: ["h6", { text: "Offline - 1" }],
|
||||
});
|
||||
await contains(".o-discuss-ChannelMember", {
|
||||
text: "Mitchell Admin",
|
||||
after: ["h6", { text: "Online - 2" }],
|
||||
before: ["h6", { text: "Offline - 1" }],
|
||||
});
|
||||
await contains(".o-discuss-ChannelMember", {
|
||||
text: "Dobby",
|
||||
after: ["h6", { text: "Offline - 1" }],
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
|
||||
defineMailModels();
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("Group name is based on channel members when name is not set", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].create(
|
||||
["Alice", "Bob", "Eve", "John", "Sam"].map((name) => ({
|
||||
name,
|
||||
partner_id: pyEnv["res.partner"].create({ name }),
|
||||
}))
|
||||
);
|
||||
const channelId = pyEnv["discuss.channel"].create({ channel_type: "group" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-DiscussContent-threadName[title='Mitchell Admin']");
|
||||
await click("button[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "Alice" });
|
||||
await click("button", { text: "Invite to Group Chat" });
|
||||
await contains(".o-mail-DiscussContent-threadName[title='Mitchell Admin and Alice']");
|
||||
await click("button[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "Bob" });
|
||||
await click("button", { text: "Invite to Group Chat" });
|
||||
await contains(".o-mail-DiscussContent-threadName[title='Mitchell Admin, Alice, and Bob']");
|
||||
await click("button[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "Eve" });
|
||||
await click("button", { text: "Invite to Group Chat" });
|
||||
await contains(
|
||||
".o-mail-DiscussContent-threadName[title='Mitchell Admin, Alice, Bob, and 1 other']"
|
||||
);
|
||||
await click("button[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "John" });
|
||||
await click("button", { text: "Invite to Group Chat" });
|
||||
await contains(
|
||||
".o-mail-DiscussContent-threadName[title='Mitchell Admin, Alice, Bob, and 2 others']"
|
||||
);
|
||||
await click(".o-mail-DiscussContent-threadName");
|
||||
await insertText(".o-mail-DiscussContent-threadName.o-focused", "Custom name", {
|
||||
replace: true,
|
||||
});
|
||||
await contains(".o-mail-DiscussContent-threadName[title='Custom name']");
|
||||
await press("Enter");
|
||||
// Ensure that after setting the name, members are not taken into account for the group name.
|
||||
await click("button[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "Sam" });
|
||||
await click("button", { text: "Invite to Group Chat" });
|
||||
await contains(".o_mail_notification", { text: "invited Sam to the channel" });
|
||||
await contains(".o-mail-DiscussContent-threadName[title='Custom name']");
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("clicking message link does not swap open chat window", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [rdId, supportId] = pyEnv["discuss.channel"].create([
|
||||
{ name: "R&D" },
|
||||
{ name: "Support" },
|
||||
]);
|
||||
const messageRdId = pyEnv["mail.message"].create({
|
||||
body: "Hello R&D",
|
||||
model: "discuss.channel",
|
||||
res_id: rdId,
|
||||
});
|
||||
const urlRd = `${getOrigin()}/mail/message/${messageRdId}`;
|
||||
const messageSupportId = pyEnv["mail.message"].create({
|
||||
body: `Hello from there <a class="o_message_redirect" href="${urlRd}" data-oe-model="mail.message" data-oe-id="${messageRdId}">${urlRd}</a>`,
|
||||
model: "discuss.channel",
|
||||
res_id: supportId,
|
||||
});
|
||||
const urlSupport = `${getOrigin()}/mail/message/${messageSupportId}`;
|
||||
pyEnv["mail.message"].create({
|
||||
body: `Hello back <a class="o_message_redirect" href="${urlSupport}" data-oe-model="mail.message" data-oe-id="${messageSupportId}">${urlSupport}</a>`,
|
||||
model: "discuss.channel",
|
||||
res_id: rdId,
|
||||
});
|
||||
setupChatHub({ opened: [rdId, supportId] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatWindow:eq(0) .o-mail-ChatWindow-header:contains(R&D)");
|
||||
await contains(".o-mail-ChatWindow:eq(1) .o-mail-ChatWindow-header:contains(Support)");
|
||||
await click("a.o_message_redirect:contains(R&D)");
|
||||
await contains(".o-mail-Message.o-highlighted:contains(Hello R&D)");
|
||||
await contains(".o-mail-ChatWindow:eq(0) .o-mail-ChatWindow-header:contains(R&D)");
|
||||
await contains(".o-mail-ChatWindow:eq(1) .o-mail-ChatWindow-header:contains(Support)");
|
||||
await click("a.o_message_redirect:contains(Support)");
|
||||
await contains(".o-mail-Message.o-highlighted:contains(Hello from there)");
|
||||
await contains(".o-mail-ChatWindow:eq(0) .o-mail-ChatWindow-header:contains(R&D)");
|
||||
await contains(".o-mail-ChatWindow:eq(1) .o-mail-ChatWindow-header:contains(Support)");
|
||||
});
|
||||
|
|
@ -0,0 +1,661 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
|
||||
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Command, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("rendering when just one has received the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const [memberId_1] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId_1],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId_1], {
|
||||
fetched_message_id: messageId,
|
||||
seen_message_id: false,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageSeenIndicator");
|
||||
await contains(".o-mail-MessageSeenIndicator[title='Sent']");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
|
||||
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
|
||||
});
|
||||
|
||||
test("rendering when everyone have received the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, {
|
||||
fetched_message_id: messageId,
|
||||
seen_message_id: false,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageSeenIndicator");
|
||||
await contains(".o-mail-MessageSeenIndicator[title='Sent']");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
|
||||
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
|
||||
});
|
||||
|
||||
test("rendering when just one has seen the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, {
|
||||
fetched_message_id: messageId,
|
||||
seen_message_id: false,
|
||||
});
|
||||
const [memberId_1] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId_1],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId_1], {
|
||||
seen_message_id: messageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageSeenIndicator");
|
||||
await contains(".o-mail-MessageSeenIndicator[title='Seen by Demo User']");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
|
||||
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
|
||||
});
|
||||
|
||||
test("rendering when just one has seen & received the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const mesageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const [memberId_1] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId_1],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId_1], {
|
||||
seen_message_id: mesageId,
|
||||
fetched_message_id: mesageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageSeenIndicator");
|
||||
await contains(".o-mail-MessageSeenIndicator[title='Seen by Demo User']");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
|
||||
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
|
||||
});
|
||||
|
||||
test("rendering when just everyone has seen the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, {
|
||||
fetched_message_id: messageId,
|
||||
seen_message_id: messageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageSeenIndicator");
|
||||
await contains(".o-mail-MessageSeenIndicator[title='Seen by everyone']");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
|
||||
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 1 });
|
||||
});
|
||||
|
||||
test("'channel_fetch' notification received is correctly handled", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "test" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 });
|
||||
|
||||
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
|
||||
// Simulate received channel fetched notification
|
||||
pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", {
|
||||
id: pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId],
|
||||
])[0],
|
||||
channel_id: channelId,
|
||||
last_message_id: 100,
|
||||
partner_id: partnerId,
|
||||
});
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
|
||||
});
|
||||
|
||||
test("mark channel as seen from the bus", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "test" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 });
|
||||
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
|
||||
// Simulate received channel seen notification
|
||||
const DiscussChannelMember = pyEnv["discuss.channel.member"];
|
||||
pyEnv["bus.bus"]._sendone(
|
||||
channel,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(
|
||||
DiscussChannelMember.browse(
|
||||
DiscussChannelMember.search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId],
|
||||
])
|
||||
),
|
||||
{ seen_message_id: messageId }
|
||||
).get_result()
|
||||
);
|
||||
await contains(".o-mail-MessageSeenIndicator[title='Seen by test']");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
|
||||
});
|
||||
|
||||
test("should display message indicator when message is fetched/seen", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Recipient" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 });
|
||||
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
|
||||
// Simulate received channel fetched notification
|
||||
pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", {
|
||||
id: pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId],
|
||||
])[0],
|
||||
channel_id: channelId,
|
||||
last_message_id: messageId,
|
||||
partner_id: partnerId,
|
||||
});
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
|
||||
// Simulate received channel seen notification
|
||||
const DiscussChannelMember = pyEnv["discuss.channel.member"];
|
||||
pyEnv["bus.bus"]._sendone(
|
||||
channel,
|
||||
"mail.record/insert",
|
||||
new mailDataHelpers.Store(
|
||||
DiscussChannelMember.browse(
|
||||
DiscussChannelMember.search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId],
|
||||
])
|
||||
),
|
||||
{ seen_message_id: messageId }
|
||||
).get_result()
|
||||
);
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
|
||||
});
|
||||
|
||||
test("do not show message seen indicator on the last message seen by everyone when the current user is not author of the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, { seen_message_id: messageId });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-MessageSeenIndicator", { count: 0 });
|
||||
});
|
||||
|
||||
test("do not show message seen indicator on all the messages of the current user that are older than the last message seen by everyone", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
const [, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Message before last seen</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
},
|
||||
{
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Last seen by everyone</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
},
|
||||
]);
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, { seen_message_id: messageId_2 });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", {
|
||||
text: "Message before last seen",
|
||||
contains: [".o-mail-MessageSeenIndicator", { contains: [".fa-check", { count: 0 }] }],
|
||||
});
|
||||
});
|
||||
|
||||
test("only show messaging seen indicator if authored by me, after last seen by all message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
res_id: channelId,
|
||||
model: "discuss.channel",
|
||||
});
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, {
|
||||
fetched_message_id: messageId,
|
||||
seen_message_id: messageId - 1,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
|
||||
});
|
||||
|
||||
test("all seen indicator in chat displayed only once (chat created by correspondent)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const demoPid = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const demoUid = pyEnv["res.users"].create({ partner_id: demoPid });
|
||||
const selfPid = serverState.partnerId;
|
||||
const channelId = await withUser(demoUid, () =>
|
||||
pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: demoPid }),
|
||||
Command.create({ partner_id: selfPid }),
|
||||
],
|
||||
})
|
||||
);
|
||||
const [, messageId] = pyEnv["mail.message"].create([
|
||||
{
|
||||
author_id: selfPid,
|
||||
body: "<p>Test1</p>",
|
||||
res_id: channelId,
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
author_id: selfPid,
|
||||
body: "<p>Test2</p>",
|
||||
res_id: channelId,
|
||||
model: "discuss.channel",
|
||||
},
|
||||
]);
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, {
|
||||
fetched_message_id: messageId,
|
||||
seen_message_id: messageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen .fa-check", { count: 2 });
|
||||
});
|
||||
|
||||
test("no seen indicator in 'channel' channels (with is_typing)", async () => {
|
||||
// is_typing info contains fetched / seen message so this could mistakenly show seen indicators
|
||||
const pyEnv = await startServer();
|
||||
const demoId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const demoUserId = pyEnv["res.users"].create({ partner_id: demoId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test-channel",
|
||||
channel_type: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: demoId }),
|
||||
],
|
||||
});
|
||||
const chatId = pyEnv["discuss.channel"].create({
|
||||
name: "test-chat",
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: demoId }),
|
||||
],
|
||||
});
|
||||
const [channelMsgId, chatMsgId] = pyEnv["mail.message"].create([
|
||||
{
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>channel-msg</p>",
|
||||
res_id: channelId,
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>chat-msg</p>",
|
||||
res_id: chatId,
|
||||
model: "discuss.channel",
|
||||
},
|
||||
]);
|
||||
const channelMemberIds = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
]);
|
||||
const chatMemberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", chatId]]);
|
||||
pyEnv["discuss.channel.member"].write(channelMemberIds, {
|
||||
fetched_message_id: channelMsgId,
|
||||
seen_message_id: 0,
|
||||
});
|
||||
pyEnv["discuss.channel.member"].write(chatMemberIds, {
|
||||
fetched_message_id: chatMsgId,
|
||||
seen_message_id: 0,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { text: "channel-msg" });
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 }); // none in channel
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "Demo User" });
|
||||
await contains(".o-mail-Message", { text: "chat-msg" });
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 }); // received in chat
|
||||
// simulate channel read by Demo User in both threads
|
||||
await withUser(demoUserId, () =>
|
||||
rpc("/discuss/channel/mark_as_read", {
|
||||
channel_id: channelId,
|
||||
last_message_id: channelMsgId,
|
||||
})
|
||||
);
|
||||
await withUser(demoUserId, () =>
|
||||
rpc("/discuss/channel/mark_as_read", {
|
||||
channel_id: chatId,
|
||||
last_message_id: chatMsgId,
|
||||
})
|
||||
);
|
||||
// simulate typing by Demo User in both threads
|
||||
await withUser(demoUserId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await withUser(demoUserId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: chatId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 }); // seen in chat
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "test-channel" });
|
||||
await contains(".o-mail-Message", { text: "channel-msg" });
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 }); // none in channel
|
||||
});
|
||||
|
||||
test("Show everyone seen title on message seen indicator", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId, last_seen_dt: "2024-06-01 12:00" }),
|
||||
Command.create({ partner_id: partnerId_1, last_seen_dt: "2024-06-01 12:00" }),
|
||||
Command.create({ partner_id: partnerId_2, last_seen_dt: "2024-06-01 13:00" }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const mesageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const [memberId_1, memberId_2] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "in", [partnerId_1, partnerId_2]],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId_1], {
|
||||
seen_message_id: mesageId,
|
||||
fetched_message_id: mesageId,
|
||||
});
|
||||
pyEnv["discuss.channel.member"].write([memberId_2], {
|
||||
seen_message_id: mesageId,
|
||||
fetched_message_id: mesageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains("[title='Seen by everyone']");
|
||||
});
|
||||
|
||||
test("Title show some member seen info (partial seen), click show dialog with full info", async () => {
|
||||
// last member flagged as not seen so that it doesn't show "Seen by everyone" but list names instead
|
||||
const pyEnv = await startServer();
|
||||
const partners = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
partners.push({ name: `User ${i}` });
|
||||
}
|
||||
const partnerIds = pyEnv["res.partner"].create(partners);
|
||||
const channelMemberIds = [];
|
||||
for (const partner_id of partnerIds) {
|
||||
channelMemberIds.push(
|
||||
Command.create({
|
||||
partner_id,
|
||||
last_seen_dt: partner_id === partnerIds.at(-1) ? false : "2024-06-01 12:00",
|
||||
})
|
||||
);
|
||||
}
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
partner_id: serverState.partnerId,
|
||||
last_seen_dt: "2024-06-01 12:00",
|
||||
}),
|
||||
...channelMemberIds,
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const mesageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "<p>Test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const members = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "in", partnerIds.filter((p) => p !== partnerIds.at(-1))],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write(members, {
|
||||
seen_message_id: mesageId,
|
||||
fetched_message_id: mesageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains("[title='Seen by User 0, User 1, User 2 and 8 others']");
|
||||
await click(".o-mail-MessageSeenIndicator");
|
||||
await contains("li", { count: 11 });
|
||||
for (let i = 0; i < 11; i++) {
|
||||
await contains("li", { text: `User ${i}` }); // Not checking datetime because HOOT mocking of tz do not work
|
||||
}
|
||||
});
|
||||
|
||||
test("Show seen indicator on message with only attachment", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
|
||||
const attachmentId = pyEnv["ir.attachment"].create({
|
||||
name: "test.txt",
|
||||
mimetype: "text/plain",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
attachment_ids: [attachmentId],
|
||||
});
|
||||
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
|
||||
pyEnv["discuss.channel.member"].write(memberIds, {
|
||||
fetched_message_id: messageId,
|
||||
seen_message_id: false,
|
||||
});
|
||||
const [memberId_1] = pyEnv["discuss.channel.member"].search([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", partnerId_1],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([memberId_1], {
|
||||
seen_message_id: messageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageSeenIndicator");
|
||||
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
|
||||
});
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
import { insertText as htmlInsertText } from "@html_editor/../tests/_helpers/user_actions";
|
||||
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
focus,
|
||||
insertText,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { Composer } from "@mail/core/common/composer";
|
||||
import { beforeEach, describe, test } from "@odoo/hoot";
|
||||
import {
|
||||
asyncStep,
|
||||
getService,
|
||||
patchWithCleanup,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
beforeEach(() => {
|
||||
// Simulate real user interactions
|
||||
patchWithCleanup(Composer.prototype, {
|
||||
isEventTrusted() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('do not send typing notification on typing "/" command', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "channel" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", () => {
|
||||
if (!testEnded) {
|
||||
asyncStep("notify_typing");
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "/");
|
||||
await contains(".o-mail-Composer button[title='Send']:enabled");
|
||||
await waitForSteps([]); // No rpc done
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test('do not send typing notification on typing after selecting suggestion from "/" command', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "channel" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", () => {
|
||||
if (!testEnded) {
|
||||
asyncStep("notify_typing");
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "/");
|
||||
await click(":nth-child(1 of .o-mail-Composer-suggestion)");
|
||||
await contains(".o-mail-Composer-suggestion strong", { count: 0 });
|
||||
await insertText(".o-mail-Composer-input", " is user?");
|
||||
await waitForSteps([]); // No rpc done"
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test("send is_typing on adding emoji", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "channel" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", () => {
|
||||
if (!testEnded) {
|
||||
asyncStep("notify_typing");
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await insertText("input[placeholder='Search emoji']", "Santa Claus");
|
||||
await click(".o-Emoji", { text: "🎅" });
|
||||
await waitForSteps(["notify_typing"]);
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test("add an emoji after a command", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-input", { value: "" });
|
||||
await insertText(".o-mail-Composer-input", "/");
|
||||
await click(":nth-child(1 of .o-mail-Composer-suggestion)");
|
||||
await contains(".o-mail-Composer-input", { value: "/who " });
|
||||
await click("button[title='Add Emojis']");
|
||||
await click(".o-Emoji", { text: "😊" });
|
||||
await contains(".o-mail-Composer-input", { value: "/who 😊" });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("html composer: send a message in a channel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-input", { value: "" });
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await focus(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "Hello");
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "Hello" });
|
||||
await click(".o-mail-Composer button[title='Send']:enabled");
|
||||
await click(".o-mail-Message[data-persistent]:contains(Hello)");
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "" });
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Command, getService, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Add member to channel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const userId = pyEnv["res.users"].create({ name: "Harry" });
|
||||
pyEnv["res.partner"].create({ name: "Harry", user_ids: [userId] });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await contains(".o-discuss-ChannelMember", { text: "Mitchell Admin" });
|
||||
await click("[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "Harry" });
|
||||
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
|
||||
await contains(".o-discuss-ChannelInvitation", { count: 0 });
|
||||
await click("[title='Members']");
|
||||
await contains(".o-discuss-ChannelMember", { text: "Harry" });
|
||||
});
|
||||
|
||||
test("Remove member from channel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Harry" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
name: "Harry",
|
||||
user_ids: [userId],
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMember", { text: "Harry" });
|
||||
withUser(userId, () =>
|
||||
getService("orm").call("discuss.channel", "action_unfollow", [channelId])
|
||||
);
|
||||
await contains(".o-discuss-ChannelMember", { count: 0, text: "Harry" });
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { onWebsocketEvent } from "@bus/../tests/mock_websocket";
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { tick } from "@odoo/hoot-dom";
|
||||
import { makeMockEnv } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Member list and Pinned Messages Panel menu are exclusive", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // member list open by default
|
||||
await click("[title='Pinned Messages']");
|
||||
await contains(".o-discuss-PinnedMessagesPanel");
|
||||
await contains(".o-discuss-ChannelMemberList", { count: 0 });
|
||||
});
|
||||
|
||||
test("subscribe to presence channels according to store data", async () => {
|
||||
const env = await makeMockEnv();
|
||||
const store = env.services["mail.store"];
|
||||
onWebsocketEvent("subscribe", (data) => expect.step(`subscribe - [${data.channels}]`));
|
||||
expect(env.services.bus_service.isActive).toBe(false);
|
||||
// Should not subscribe to presences as bus service is not started.
|
||||
store["res.partner"].insert({ id: 1, name: "Partner 1" });
|
||||
store["res.partner"].insert({ id: 2, name: "Partner 2" });
|
||||
await tick();
|
||||
expect.waitForSteps([]);
|
||||
// Starting the bus should subscribe to known presence channels.
|
||||
env.services.bus_service.start();
|
||||
await expect.waitForSteps([
|
||||
"subscribe - [odoo-presence-res.partner_1,odoo-presence-res.partner_2]",
|
||||
]);
|
||||
// Discovering new presence channels should refresh the subscription.
|
||||
store["mail.guest"].insert({ id: 1 });
|
||||
await expect.waitForSteps([
|
||||
"subscribe - [odoo-presence-mail.guest_1,odoo-presence-res.partner_1,odoo-presence-res.partner_2]",
|
||||
]);
|
||||
// Updating "im_status_access_token" should refresh the subscription.
|
||||
store["mail.guest"].insert({ id: 1, im_status_access_token: "token" });
|
||||
await expect.waitForSteps([
|
||||
"subscribe - [odoo-presence-mail.guest_1-token,odoo-presence-res.partner_1,odoo-presence-res.partner_2]",
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
observeRenders,
|
||||
openDiscuss,
|
||||
prepareObserveRenders,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { Composer } from "@mail/core/common/composer";
|
||||
import { Message } from "@mail/core/common/message";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { onMounted, onPatched } from "@odoo/owl";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { range } from "@web/core/utils/numbers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("posting new message should only render relevant part", async () => {
|
||||
// For example, it should not render old messages again
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const messageIds = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
messageIds.push(
|
||||
pyEnv["mail.message"].create({
|
||||
body: `not_empty_${i}`,
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
})
|
||||
);
|
||||
}
|
||||
messageIds.pop(); // remove last as it might need re-render (it was the newest message before)
|
||||
let posting = false;
|
||||
prepareObserveRenders();
|
||||
patchWithCleanup(Message.prototype, {
|
||||
setup() {
|
||||
const cb = () => {
|
||||
if (posting) {
|
||||
if (messageIds.includes(this.message.id)) {
|
||||
throw new Error(
|
||||
"Should not re-render old messages again on posting a new message"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
onMounted(cb);
|
||||
onPatched(cb);
|
||||
return super.setup();
|
||||
},
|
||||
});
|
||||
await start();
|
||||
const stopObserve1 = observeRenders();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 10 });
|
||||
await insertText(".o-mail-Composer-input", "Test");
|
||||
const result1 = stopObserve1();
|
||||
// LessThan because renders could be batched
|
||||
expect(result1.get(Message)).toBeLessThan(11); // 10: all messages initially
|
||||
const stopObserve2 = observeRenders();
|
||||
posting = true;
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-Message", { count: 11 });
|
||||
posting = false;
|
||||
const result2 = stopObserve2();
|
||||
expect(result2.get(Composer)).toBeLessThan(3); // 2: temp disabling + clear content
|
||||
expect(result2.get(Message)).toBeLessThan(4); // 3: new temp msg + new genuine msg + prev msg
|
||||
});
|
||||
|
||||
test("replying to message should only render relevant part", async () => {
|
||||
// For example, it should not render all messages when selecting message to reply
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const messageIds = range(0, 10).map((i) =>
|
||||
pyEnv["mail.message"].create({ body: `${i}`, model: "discuss.channel", res_id: channelId })
|
||||
);
|
||||
messageIds.pop(); // remove last as this is the one to be replied to
|
||||
let replying = false;
|
||||
prepareObserveRenders();
|
||||
patchWithCleanup(Message.prototype, {
|
||||
setup() {
|
||||
const cb = () => {
|
||||
if (replying) {
|
||||
if (messageIds.includes(this.message.id)) {
|
||||
throw new Error(
|
||||
"Should not re-render other messages on replying to a message"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
onMounted(cb);
|
||||
onPatched(cb);
|
||||
return super.setup();
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 10 });
|
||||
const stopObserve = observeRenders();
|
||||
replying = true;
|
||||
await click(".o-mail-Message:last [title='Reply']");
|
||||
await contains(".o-mail-Composer:has(:text('Replying to Mitchell Admin'))");
|
||||
replying = false;
|
||||
const result = stopObserve();
|
||||
expect(result.get(Composer)).toBeLessThan(2);
|
||||
expect(result.get(Message)).toBeLessThan(2);
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { waitUntilSubscribe } from "@bus/../tests/bus_test_helpers";
|
||||
import {
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Command, mockService, patchWithCleanup, withUser } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("open channel in discuss from push notification", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-DiscussContent-threadName[title='Inbox']");
|
||||
navigator.serviceWorker.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: { action: "OPEN_CHANNEL", data: { id: channelId } },
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-DiscussContent-threadName[title='General']");
|
||||
});
|
||||
|
||||
test("notify message to user as non member", async () => {
|
||||
patchWithCleanup(window, {
|
||||
Notification: class Notification {
|
||||
static get permission() {
|
||||
return "granted";
|
||||
}
|
||||
constructor() {
|
||||
expect.step("push notification");
|
||||
}
|
||||
addEventListener() {}
|
||||
},
|
||||
});
|
||||
mockService("multi_tab", { isOnMainTab: () => true });
|
||||
const pyEnv = await startServer();
|
||||
const johnUser = pyEnv["res.users"].create({ name: "John" });
|
||||
const johnPartner = pyEnv["res.partner"].create({ name: "John", user_ids: [johnUser] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [Command.create({ partner_id: johnPartner })],
|
||||
});
|
||||
await start();
|
||||
await Promise.all([openDiscuss(channelId), waitUntilSubscribe(`discuss.channel_${channelId}`)]);
|
||||
await withUser(johnUser, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "Hello!", message_type: "comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-Message", { text: "Hello!" });
|
||||
expect.verifySteps(["push notification"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
onRpcAfter,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Deferred, mockDate, animationFrame } from "@odoo/hoot-mock";
|
||||
import { Command, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("navigate to sub channel", async () => {
|
||||
mockDate("2025-01-01 12:00:00", +1);
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// Should access sub-thread after its creation.
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
|
||||
await click("button[title='Threads']");
|
||||
await click("button[aria-label='Create Thread']");
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
|
||||
// Should access sub-thread when clicking on the menu.
|
||||
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
|
||||
await click("button[title='Threads']");
|
||||
await click(".o-mail-SubChannelPreview", { text: "New Thread" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
|
||||
// Should access sub-thread when clicking on the notification.
|
||||
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
|
||||
await contains(".o-mail-NotificationMessage", {
|
||||
text: `${serverState.partnerName} started a thread: New Thread.1:00 PM`,
|
||||
});
|
||||
await click(".o-mail-NotificationMessage a", { text: "New Thread" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
|
||||
});
|
||||
|
||||
test("can manually unpin a sub-thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// Open thread so this is pinned
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
|
||||
await click("button[title='Threads']");
|
||||
await click("button[aria-label='Create Thread']");
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
|
||||
await click("[title='Thread Actions']");
|
||||
await click(".o-dropdown-item:contains('Unpin Conversation')");
|
||||
await contains(".o-mail-DiscussSidebar-item", { text: "New Thread", count: 0 });
|
||||
});
|
||||
|
||||
test("create sub thread from existing message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
body: "<p>Selling a training session and selling the products after the training session is more efficient.</p>",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Message-actions [title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Create Thread')");
|
||||
await contains(".o-mail-DiscussContent-threadName", {
|
||||
value: "Selling a training session and",
|
||||
});
|
||||
await contains(".o-mail-Message", {
|
||||
text: "Selling a training session and selling the products after the training session is more efficient.",
|
||||
});
|
||||
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
|
||||
await click(".o-mail-Message-actions [title='Expand']");
|
||||
await contains(".o-dropdown-item:contains('Create Thread')", { count: 0 });
|
||||
await contains(".o-mail-SubChannelPreview:contains('Selling a training session and')");
|
||||
await click(".o-mail-SubChannelPreview:contains('Selling a training session and')");
|
||||
await contains(".o-mail-DiscussContent-threadName", {
|
||||
value: "Selling a training session and",
|
||||
});
|
||||
await contains(".o-mail-SubChannelPreview:contains('Selling a training session and')", {
|
||||
count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("should allow creating a thread from an existing thread", async () => {
|
||||
mockDate("2025-01-01 12:00:00", +1);
|
||||
const pyEnv = await startServer();
|
||||
const parent_channel_id = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const sub_channel_id = pyEnv["discuss.channel"].create({
|
||||
name: "sub channel",
|
||||
parent_channel_id: parent_channel_id,
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
model: "discuss.channel",
|
||||
res_id: sub_channel_id,
|
||||
body: "<p>hello alex</p>",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(sub_channel_id);
|
||||
await click(".o-mail-Message-actions [title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Create Thread')");
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "hello alex" });
|
||||
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
|
||||
await contains(".o-mail-NotificationMessage", {
|
||||
text: `${serverState.partnerName} started a thread: hello alex.1:00 PM`,
|
||||
});
|
||||
});
|
||||
|
||||
test("create sub thread from existing message (slow network)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
body: "<p>Selling a training session and selling the products after the training session is more efficient.</p>",
|
||||
});
|
||||
const createSubChannelDef = new Deferred();
|
||||
onRpcAfter("/discuss/channel/sub_channel/create", async () => await createSubChannelDef);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Message-actions [title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Create Thread')");
|
||||
await animationFrame();
|
||||
createSubChannelDef.resolve();
|
||||
await contains(".o-mail-DiscussContent-threadName", {
|
||||
value: "Selling a training session and",
|
||||
});
|
||||
await contains(".o-mail-Message", {
|
||||
text: "Selling a training session and selling the products after the training session is more efficient.",
|
||||
});
|
||||
});
|
||||
|
||||
test("create sub thread from sub-thread list", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click("button[title='Threads']");
|
||||
await contains(".o-mail-SubChannelList", { text: "This channel has no thread yet." });
|
||||
await click("button[aria-label='Create Thread']");
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
|
||||
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
|
||||
await click(".o-mail-DiscussContent-header button[title='Threads']");
|
||||
await insertText(
|
||||
".o-mail-ActionPanel:has(.o-mail-SubChannelList) .o_searchview_input",
|
||||
"MyEpicThread"
|
||||
);
|
||||
await click("button[aria-label='Search button']");
|
||||
await contains(".o-mail-SubChannelList", { text: 'No thread named "MyEpicThread"' });
|
||||
await click("button[aria-label='Create Thread']");
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "MyEpicThread" });
|
||||
});
|
||||
|
||||
test("'Thread' menu available in threads", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
});
|
||||
const subChannelID = pyEnv["discuss.channel"].create({
|
||||
name: "ThreadOne",
|
||||
parent_channel_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(subChannelID);
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "ThreadOne" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "ThreadOne" });
|
||||
await click("button[title='Threads']");
|
||||
await insertText(".o-mail-ActionPanel input[placeholder='Search by name']", "ThreadTwo");
|
||||
await click(".o-mail-ActionPanel button", { text: "Create" });
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "ThreadTwo" });
|
||||
});
|
||||
|
||||
test("sub thread is available for channel and group, not for chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
});
|
||||
pyEnv["discuss.channel"].create({
|
||||
name: "Group",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
pyEnv["discuss.channel"].create({
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click("button[title='Threads']");
|
||||
await insertText(
|
||||
".o-mail-ActionPanel input[placeholder='Search by name']",
|
||||
"Sub thread for channel"
|
||||
);
|
||||
await click(".o-mail-ActionPanel button", { text: "Create" });
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "Sub thread for channel" });
|
||||
await click(".o-mail-DiscussSidebarChannel", { text: "Group" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "Group" });
|
||||
await click("button[title='Threads']");
|
||||
await insertText(
|
||||
".o-mail-ActionPanel input[placeholder='Search by name']",
|
||||
"Sub thread for group"
|
||||
);
|
||||
await click(".o-mail-ActionPanel button", { text: "Create" });
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "Sub thread for group" });
|
||||
await click(".o-mail-DiscussSidebarChannel", { text: "Demo" });
|
||||
await contains("button[title='Threads']", { count: 0 });
|
||||
});
|
||||
|
||||
test("mention suggestions in thread match channel restrictions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const groupId = pyEnv["res.groups"].create({ name: "testGroup" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
group_public_id: groupId,
|
||||
});
|
||||
pyEnv["discuss.channel"].create({
|
||||
name: "Thread",
|
||||
parent_channel_id: channelId,
|
||||
});
|
||||
pyEnv["res.users"].write(serverState.userId, { group_ids: [Command.link(groupId)] });
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ email: "p1@odoo.com", name: "p1" },
|
||||
{ email: "p2@odoo.com", name: "p2" },
|
||||
]);
|
||||
pyEnv["res.users"].create([
|
||||
{ partner_id: partnerId_1, group_ids: [Command.link(groupId)] },
|
||||
{ partner_id: partnerId_2 },
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-DiscussSidebar-item.o-active:contains('General')");
|
||||
await insertText(".o-mail-Composer-input", "@");
|
||||
await contains(".o-mail-Composer-suggestion", { count: 2 });
|
||||
await contains(".o-mail-Composer-suggestion", { text: "Mitchell Admin" });
|
||||
await contains(".o-mail-Composer-suggestion", { text: "p1" });
|
||||
await click(".o-mail-DiscussSidebar-item:contains('Thread')");
|
||||
await contains(".o-mail-DiscussSidebar-item.o-active:contains('Thread')");
|
||||
await insertText(".o-mail-Composer-input", "@");
|
||||
await contains(".o-mail-Composer-suggestion", { count: 2 });
|
||||
await contains(".o-mail-Composer-suggestion", { text: "Mitchell Admin" });
|
||||
await contains(".o-mail-Composer-suggestion", { text: "p1" });
|
||||
});
|
||||
|
||||
test("sub-thread is visually muted when mute is active", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
|
||||
await click("button[title='Threads']");
|
||||
await click("button[aria-label='Create Thread']");
|
||||
await contains(".opacity-50.o-mail-DiscussSidebar-item:contains('New Thread')", { count: 0 });
|
||||
await click(".o-mail-DiscussSidebar-item:contains('New Thread')");
|
||||
await click("button[title='Notification Settings']");
|
||||
await click("button:contains('Mute Conversation')");
|
||||
await click("button:contains('Until I turn it back on')");
|
||||
await contains(".opacity-50.o-mail-DiscussSidebar-item:contains('New Thread')");
|
||||
});
|
||||
|
||||
test("muted channel hides sub-thread unless channel is selected or thread has unread messages", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const partnerId2 = pyEnv["res.partner"].create({ email: "p1@odoo.com", name: "p1" });
|
||||
const userId2 = pyEnv["res.users"].create({ name: "User 2", partner_id: partnerId2 });
|
||||
const partnerId = serverState.partnerId;
|
||||
const subChannelId = pyEnv["discuss.channel"].create({
|
||||
name: "New Thread",
|
||||
parent_channel_id: channelId,
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: partnerId }),
|
||||
Command.create({ partner_id: partnerId2 }),
|
||||
],
|
||||
});
|
||||
pyEnv["discuss.channel"].create({ name: "Other" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-DiscussSidebar-item:contains('General')");
|
||||
await click("button[title='Notification Settings']");
|
||||
await click("button:contains('Mute Conversation')");
|
||||
await click("button:contains('Until I turn it back on')");
|
||||
await click(".o-mail-DiscussSidebar-item:contains('Other')");
|
||||
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')", { count: 0 });
|
||||
await click(".o-mail-DiscussSidebar-item:contains('General')");
|
||||
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')");
|
||||
await click(".o-mail-DiscussSidebar-item:contains('Other')");
|
||||
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')", { count: 0 });
|
||||
withUser(userId2, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "Some message", message_type: "comment" },
|
||||
thread_id: subChannelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')");
|
||||
});
|
||||
|
||||
test("show notification when clicking on deleted thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Test Channel" });
|
||||
const activeThreadId = pyEnv["discuss.channel"].create({
|
||||
name: "Message 1",
|
||||
parent_channel_id: channelId,
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: `<div class="o_mail_notification"> started a thread:<a href="#" class="o_channel_redirect" data-oe-id="${activeThreadId}" data-oe-model="discuss.channel">Message 1</a></div>`,
|
||||
message_type: "notification",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["discuss.channel"].unlink(activeThreadId);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-NotificationMessage a", { text: "Message 1" });
|
||||
await contains(".o_notification:has(.o_notification_bar.bg-danger)", {
|
||||
text: "This thread is no longer available.",
|
||||
});
|
||||
});
|
||||
|
||||
test("Can delete channel thread as author of thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const subChannelID = pyEnv["discuss.channel"].create({
|
||||
name: "test thread",
|
||||
parent_channel_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(subChannelID);
|
||||
await contains(".o-mail-DiscussContent-threadName[title='test thread']");
|
||||
await click(".o-mail-DiscussSidebar-item:contains('test thread') [title='Thread Actions']");
|
||||
await click(".o-dropdown-item:contains('Delete Thread')");
|
||||
await click(".modal button:contains('Delete Thread')");
|
||||
await contains(".o-mail-DiscussContent-threadName[title='General']");
|
||||
await contains(
|
||||
`.o-mail-NotificationMessage :text(Mitchell Admin deleted the thread "test thread")`
|
||||
);
|
||||
});
|
||||
|
||||
test("can mention all group chat members inside its sub-thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Lilibeth" });
|
||||
const groupChannelId = pyEnv["discuss.channel"].create({
|
||||
name: "Our channel",
|
||||
channel_type: "group",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
const groupSubChannelId = pyEnv["discuss.channel"].create({
|
||||
name: "New Thread",
|
||||
parent_channel_id: groupChannelId,
|
||||
channel_member_ids: [Command.create({ partner_id: serverState.partnerId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(groupSubChannelId);
|
||||
await insertText(".o-mail-Composer-input", "@");
|
||||
await contains(".o-mail-Composer-suggestion", { count: 2 });
|
||||
});
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
import { insertText as htmlInsertText } from "@html_editor/../tests/_helpers/user_actions";
|
||||
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { beforeEach, describe, test } from "@odoo/hoot";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import { Command, getService, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Composer } from "@mail/core/common/composer";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
beforeEach(() => {
|
||||
// Simulate real user interactions
|
||||
patchWithCleanup(Composer.prototype, {
|
||||
isEventTrusted() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('[text composer] display command suggestions on typing "/"', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-suggestionList");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await insertText(".o-mail-Composer-input", "/");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open");
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("display command suggestions on typing '/'", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-suggestionList");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await focus(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "/");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open");
|
||||
});
|
||||
|
||||
test("[text composer] use a command for a specific channel type", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ channel_type: "chat" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-suggestionList");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await contains(".o-mail-Composer-input", { value: "" });
|
||||
await insertText(".o-mail-Composer-input", "/");
|
||||
await click(".o-mail-Composer-suggestion strong", { text: "who" });
|
||||
await contains(".o-mail-Composer-input", { value: "/who " });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("use a command for a specific channel type", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ channel_type: "chat" });
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-suggestionList");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await focus(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "/");
|
||||
await click(".o-mail-Composer-suggestion strong", { text: "who" });
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "/who" });
|
||||
});
|
||||
|
||||
test("[text composer] command suggestion should only open if command is the first character", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-suggestionList");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await contains(".o-mail-Composer-input", { value: "" });
|
||||
await insertText(".o-mail-Composer-input", "bluhbluh ");
|
||||
await contains(".o-mail-Composer-input", { value: "bluhbluh " });
|
||||
await insertText(".o-mail-Composer-input", "/");
|
||||
// weak test, no guarantee that we waited long enough for the potential list to open
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("command suggestion should only open if command is the first character", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-suggestionList");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await focus(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "bluhbluh");
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "bluhbluh" });
|
||||
await htmlInsertText(editor, "/");
|
||||
// weak test, no guarantee that we waited long enough for the potential list to open
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
});
|
||||
|
||||
test("Sort partner suggestions by recent chats", async () => {
|
||||
mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!)
|
||||
const pyEnv = await startServer();
|
||||
const [partner_1, partner_2, partner_3] = pyEnv["res.partner"].create([
|
||||
{ name: "User 1" },
|
||||
{ name: "User 2" },
|
||||
{ name: "User 3" },
|
||||
]);
|
||||
pyEnv["res.users"].create([
|
||||
{ partner_id: partner_1 },
|
||||
{ partner_id: partner_2 },
|
||||
{ partner_id: partner_3 },
|
||||
]);
|
||||
pyEnv["discuss.channel"].create([
|
||||
{
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partner_1 }),
|
||||
Command.create({ partner_id: partner_2 }),
|
||||
Command.create({ partner_id: partner_3 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
last_interest_dt: "2023-01-01 00:00:00",
|
||||
partner_id: serverState.partnerId,
|
||||
}),
|
||||
Command.create({ partner_id: partner_1 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
last_interest_dt: "2023-01-01 00:00:10",
|
||||
partner_id: serverState.partnerId,
|
||||
}),
|
||||
Command.create({ partner_id: partner_2 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
last_interest_dt: "2023-01-01 00:00:20",
|
||||
partner_id: serverState.partnerId,
|
||||
}),
|
||||
Command.create({ partner_id: partner_3 }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click(".o-mail-DiscussSidebarChannel", { text: "User 2" });
|
||||
await insertText(".o-mail-Composer-input", "This is a test");
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message-content", { text: "This is a test" });
|
||||
await click(".o-mail-DiscussSidebarChannel", { text: "General" });
|
||||
await contains(
|
||||
".o-mail-DiscussSidebarCategory-chat + .o-mail-DiscussSidebarChannel-container:text(User 2)"
|
||||
);
|
||||
await insertText(".o-mail-Composer-input[placeholder='Message #General…']", "@");
|
||||
await insertText(".o-mail-Composer-input", "User");
|
||||
await contains(".o-mail-Composer-suggestion strong", { count: 3 });
|
||||
await contains(":nth-child(1 of .o-mail-Composer-suggestion) strong", { text: "User 2" });
|
||||
await contains(":nth-child(2 of .o-mail-Composer-suggestion) strong", { text: "User 3" });
|
||||
await contains(":nth-child(3 of .o-mail-Composer-suggestion) strong", { text: "User 1" });
|
||||
});
|
||||
|
||||
test("mention suggestion are shown after deleting a character", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "@John D");
|
||||
await contains(".o-mail-Composer-suggestion strong", { text: "John Doe" });
|
||||
await insertText(".o-mail-Composer-input", "a");
|
||||
await contains(".o-mail-Composer-suggestion strong", { count: 0, text: "John D" });
|
||||
// Simulate pressing backspace
|
||||
const textarea = document.querySelector(".o-mail-Composer-input");
|
||||
textarea.value = textarea.value.slice(0, -1);
|
||||
await contains(".o-mail-Composer-suggestion strong", { text: "John Doe" });
|
||||
});
|
||||
|
||||
test("[text composer] command suggestion are shown after deleting a character", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "/he");
|
||||
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
|
||||
await insertText(".o-mail-Composer-input", "e");
|
||||
await contains(".o-mail-Composer-suggestion strong", { count: 0, text: "help" });
|
||||
// Simulate pressing backspace
|
||||
const textarea = document.querySelector(".o-mail-Composer-input");
|
||||
textarea.value = textarea.value.slice(0, -1);
|
||||
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("command suggestion are shown after deleting a character", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-suggestionList");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await focus(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "/he");
|
||||
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
|
||||
await htmlInsertText(editor, "e");
|
||||
await contains(".o-mail-Composer-suggestion strong", { count: 0, text: "help" });
|
||||
await press("Backspace");
|
||||
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
|
||||
});
|
||||
|
||||
test("mention suggestion displays OdooBot before archived partners", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Jane", active: false });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "Our channel",
|
||||
channel_type: "group",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
Command.create({ partner_id: serverState.odoobotId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "@");
|
||||
await contains(".o-mail-Composer-suggestion", { count: 3 });
|
||||
await contains(".o-mail-Composer-suggestion", {
|
||||
text: "Mitchell Admin",
|
||||
before: [
|
||||
".o-mail-Composer-suggestion",
|
||||
{
|
||||
text: "OdooBot",
|
||||
before: [".o-mail-Composer-suggestion", { text: "Jane" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Command, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("can open DM from @username in command palette", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const marioUid = pyEnv["res.users"].create({ name: "Mario" });
|
||||
pyEnv["res.partner"].create({ name: "Mario", user_ids: [marioUid] });
|
||||
await start();
|
||||
triggerHotkey("control+k");
|
||||
await insertText(".o_command_palette_search input", "@");
|
||||
await insertText("input[placeholder='Search a conversation']", "Mario");
|
||||
await click(".o_command.focused:has(.oi-user)", { text: "Mario" });
|
||||
await contains(".o-mail-ChatWindow", { text: "Mario" });
|
||||
});
|
||||
|
||||
test("can open channel from @channel_name in command palette", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({
|
||||
name: "general",
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
partner_id: serverState.partnerId,
|
||||
last_interest_dt: "2021-01-02 10:00:00", // same last interest to sort by id
|
||||
}),
|
||||
],
|
||||
});
|
||||
pyEnv["discuss.channel"].create({
|
||||
name: "project",
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
partner_id: serverState.partnerId,
|
||||
last_interest_dt: "2021-01-02 10:00:00", // same last interest to sort by id
|
||||
}),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
triggerHotkey("control+k");
|
||||
await insertText(".o_command_palette_search input", "@");
|
||||
await contains(".o_command", { count: 6 });
|
||||
await contains(".o_command:eq(0):has(.fa-hashtag)", { text: "project" });
|
||||
await contains(".o_command:eq(1):has(.fa-hashtag)", { text: "general" });
|
||||
await contains(".o_command:has(.oi-user)", { text: "OdooBot" });
|
||||
await contains(".o_command:has(.oi-user)", { text: "Mitchell Admin" }); // self-conversation
|
||||
await contains(".o_command", { text: "Create Channel" });
|
||||
await contains(".o_command", { text: "Create Chat" });
|
||||
await click(".o_command.focused:has(.fa-hashtag)", { text: "project" });
|
||||
await contains(".o-mail-ChatWindow", { text: "project" });
|
||||
});
|
||||
|
||||
test("Conversation mentions in the command palette with @", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "group",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
body: "@Mitchell Admin",
|
||||
needaction: true,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
triggerHotkey("control+k");
|
||||
await insertText(".o_command_palette_search input", "@", { replace: true });
|
||||
await contains(".o_command_palette .o_command_category", {
|
||||
contains: [
|
||||
["span.fw-bold", { text: "Mentions" }],
|
||||
[".o_command.focused .o_command_name", { text: "Mitchell Admin and Mario" }],
|
||||
],
|
||||
});
|
||||
// can also make self conversation
|
||||
await contains(".o_command_palette .o_command_category", {
|
||||
contains: [[".o_command_name", { text: "Mitchell Admin" }]],
|
||||
});
|
||||
await click(".o_command.focused");
|
||||
await contains(".o-mail-ChatWindow", { text: "Mitchell Admin and Mario" });
|
||||
});
|
||||
|
||||
test("Max 3 most recent conversations in command palette of Discuss", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({ name: "channel_1" });
|
||||
pyEnv["discuss.channel"].create({ name: "channel_2" });
|
||||
pyEnv["discuss.channel"].create({ name: "channel_3" });
|
||||
pyEnv["discuss.channel"].create({ name: "channel_4" });
|
||||
await start();
|
||||
triggerHotkey("control+k");
|
||||
await insertText(".o_command_palette_search input", "@", { replace: true });
|
||||
await contains(".o_command_palette .o_command_category", {
|
||||
contains: [
|
||||
["span.fw-bold", { text: "Recent" }],
|
||||
[".o_command", { count: 3 }],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("only partners with dedicated users will be displayed in command palette", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const demoUid = pyEnv["res.users"].create({ name: "Demo" });
|
||||
pyEnv["res.partner"].create({ name: "Demo", user_ids: [demoUid] });
|
||||
pyEnv["res.partner"].create({ name: "Portal" });
|
||||
await start();
|
||||
triggerHotkey("control+k");
|
||||
await insertText(".o_command_palette_search input", "@");
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await contains(".o_command_name", { text: "Demo" });
|
||||
await contains(".o_command_name", { text: "OdooBot" });
|
||||
await contains(".o_command_name", { text: "Mitchell Admin" }); // self-conversation
|
||||
await contains(".o_command_name", { text: "Create Channel" });
|
||||
await contains(".o_command_name", { text: "Create Chat" });
|
||||
await contains(".o_command_name", { text: "Portal", count: 0 });
|
||||
});
|
||||
|
||||
test("hide conversations in recent if they have mentions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: serverState.odoobotId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
body: "@OdooBot",
|
||||
});
|
||||
await start();
|
||||
triggerHotkey("control+k");
|
||||
await insertText(".o_command_palette_search input", "@", { replace: true });
|
||||
await contains(".o_command_category span.fw-bold", { text: "Mentions" });
|
||||
await contains(".o_command_palette .o_command_category .o_command_name", {
|
||||
text: "OdooBot",
|
||||
count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test("Ctrl-K opens @ command palette in discuss app", async () => {
|
||||
await start();
|
||||
await openDiscuss();
|
||||
triggerHotkey("control+k");
|
||||
await contains(".o_command_palette_search", { text: "@" });
|
||||
});
|
||||
|
||||
test("Can create group chat from ctrl-k without any user selected", async () => {
|
||||
await start();
|
||||
await openDiscuss();
|
||||
triggerHotkey("control+k");
|
||||
await click(".o_command_name:contains(Create Chat)");
|
||||
await click(".modal-footer > .btn:contains(Create Group Chat)");
|
||||
await contains(".o-mail-DiscussSidebarChannel-itemName", { text: "Mitchell Admin" });
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { asyncStep, mockService, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Channel subscription is renewed when channel is manually added", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General", channel_member_ids: [] });
|
||||
await start();
|
||||
mockService("bus_service", {
|
||||
forceUpdateChannels() {
|
||||
asyncStep("update-channels");
|
||||
},
|
||||
});
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click("[title='Invite People']");
|
||||
await click(".o-discuss-ChannelInvitation-selectable", { text: "Mitchell Admin" });
|
||||
await click("[title='Invite']:enabled");
|
||||
await waitForSteps(["update-channels"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
listenStoreFetch,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
STORE_FETCH_ROUTES,
|
||||
triggerHotkey,
|
||||
waitStoreFetch,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import {
|
||||
asyncStep,
|
||||
Command,
|
||||
onRpc,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { pick } from "@web/core/utils/objects";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("can create a new channel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
onRpcBefore((route, args) => {
|
||||
if (
|
||||
(route.startsWith("/mail") || route.startsWith("/discuss")) &&
|
||||
!STORE_FETCH_ROUTES.includes(route)
|
||||
) {
|
||||
asyncStep(`${route} - ${JSON.stringify(args)}`);
|
||||
}
|
||||
});
|
||||
listenStoreFetch(undefined, { logParams: ["/discuss/create_channel"] });
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await waitStoreFetch([
|
||||
"failures",
|
||||
"systray_get_activities",
|
||||
"init_messaging",
|
||||
"channels_as_member",
|
||||
]);
|
||||
await contains(".o-mail-Discuss");
|
||||
await contains(".o-mail-DiscussSidebar-item", { text: "abc", count: 0 });
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await insertText("input[placeholder='Search a conversation']", "abc");
|
||||
await waitForSteps([`/discuss/search - {"term":""}`, `/discuss/search - {"term":"abc"}`]);
|
||||
await click("a", { text: "Create Channel" });
|
||||
await contains(".o-mail-DiscussSidebar-item", { text: "abc" });
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
const [channelId] = pyEnv["discuss.channel"].search([["name", "=", "abc"]]);
|
||||
const [selfMember] = pyEnv["discuss.channel.member"].search_read([
|
||||
["channel_id", "=", channelId],
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
]);
|
||||
await waitStoreFetch([["/discuss/create_channel", { name: "abc" }]], {
|
||||
stepsAfter: [
|
||||
`/discuss/channel/messages - ${JSON.stringify({
|
||||
channel_id: channelId,
|
||||
fetch_params: { limit: 60, around: selfMember.new_message_separator },
|
||||
})}`,
|
||||
`/discuss/channel/members - ${JSON.stringify({
|
||||
channel_id: channelId,
|
||||
known_member_ids: [selfMember.id],
|
||||
})}`,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("can make a DM chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
onRpcBefore((route, args) => {
|
||||
if (
|
||||
(route.startsWith("/mail") || route.startsWith("/discuss")) &&
|
||||
!STORE_FETCH_ROUTES.includes(route)
|
||||
) {
|
||||
asyncStep(`${route} - ${JSON.stringify(args)}`);
|
||||
}
|
||||
});
|
||||
onRpc((params) => {
|
||||
if (params.model === "discuss.channel" && ["search_read"].includes(params.method)) {
|
||||
asyncStep(
|
||||
`${params.route} - ${JSON.stringify(
|
||||
pick(params, "args", "kwargs", "method", "model")
|
||||
)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
listenStoreFetch(undefined, {
|
||||
logParams: ["/discuss/get_or_create_chat"],
|
||||
});
|
||||
await start();
|
||||
await waitStoreFetch(["failures", "systray_get_activities", "init_messaging"]);
|
||||
await openDiscuss();
|
||||
await waitStoreFetch(["channels_as_member"]);
|
||||
await contains(".o-mail-Discuss");
|
||||
await contains(".o-mail-DiscussSidebar-item", { text: "Mario", count: 0 });
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await insertText("input[placeholder='Search a conversation']", "mario");
|
||||
await contains(".o_command_name", { count: 3 });
|
||||
await click(".o_command_name", { text: "Mario" });
|
||||
await contains(".o-mail-DiscussSidebar-item", { text: "Mario" });
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
const [channelId] = pyEnv["discuss.channel"].search([["name", "=", "Mario, Mitchell Admin"]]);
|
||||
await waitStoreFetch([["/discuss/get_or_create_chat", { partners_to: [partnerId] }]], {
|
||||
stepsAfter: [
|
||||
`/discuss/channel/messages - ${JSON.stringify({
|
||||
channel_id: channelId,
|
||||
fetch_params: { limit: 60, around: 0 },
|
||||
})}`,
|
||||
],
|
||||
stepsBefore: [`/discuss/search - {"term":""}`, `/discuss/search - {"term":"mario"}`],
|
||||
});
|
||||
});
|
||||
|
||||
test("can create a group chat conversation", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "Mario" },
|
||||
{ name: "Luigi" },
|
||||
]);
|
||||
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await click("a", { text: "Create Chat" });
|
||||
await click("li", { text: "Mario" });
|
||||
await click("li", { text: "Luigi" });
|
||||
await click(".btn", { text: "Create Group Chat" });
|
||||
await contains(".o-mail-DiscussSidebarChannel");
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
});
|
||||
|
||||
test("mobile chat search should allow to create group chat", async () => {
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains("button.active", { text: "Notifications" });
|
||||
await click("button", { text: "Chats" });
|
||||
await contains(".o-mail-DiscussSearch-inputContainer");
|
||||
});
|
||||
|
||||
test("Chat is pinned on other tabs when joined", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Jerry Golay" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const env1 = await start({ asTab: true });
|
||||
const env2 = await start({ asTab: true });
|
||||
await openDiscuss(undefined, { target: env1 });
|
||||
await openDiscuss(undefined, { target: env2 });
|
||||
await click(`${env1.selector} input[placeholder='Search conversations']`);
|
||||
await contains(`${env1.selector} .o_command_name`, { count: 5 });
|
||||
await insertText(`${env1.selector} input[placeholder='Search a conversation']`, "Jer");
|
||||
await contains(`${env1.selector} .o_command_name`, { count: 3 });
|
||||
await click(`${env1.selector} .o_command_name`, { text: "Jerry Golay" });
|
||||
await contains(`${env1.selector} .o-mail-DiscussSidebar-item`, { text: "Jerry Golay" });
|
||||
await contains(`${env2.selector} .o-mail-DiscussSidebar-item`, { text: "Jerry Golay" });
|
||||
});
|
||||
|
||||
test("Auto-open OdooBot chat when opening discuss for the first time", async () => {
|
||||
// Odoobot chat has onboarding for using Discuss app.
|
||||
// We assume pinned odoobot chat without any message seen means user just started using Discuss app.
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: serverState.odoobotId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "OdooBot" });
|
||||
});
|
||||
|
||||
test("no conversation selected when opening non-existing channel in discuss", async () => {
|
||||
await startServer();
|
||||
await start();
|
||||
await openDiscuss(200); // non-existing id
|
||||
await contains("h4", { text: "No conversation selected." });
|
||||
await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-down");
|
||||
await click(".o-mail-DiscussSidebar .btn", { text: "Channels" }); // check no crash
|
||||
await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-right");
|
||||
});
|
||||
|
||||
test("can access portal partner profile from avatar popover", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const joelPartnerId = pyEnv["res.partner"].create({
|
||||
name: "Joel",
|
||||
user_ids: [Command.create({ name: "Joel", share: true })],
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: joelPartnerId }),
|
||||
],
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: joelPartnerId,
|
||||
body: "Hello!",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Message-avatar", {
|
||||
parent: [".o-mail-Message", { text: "Joel" }],
|
||||
});
|
||||
await contains(".o_avatar_card", { text: "Joel" });
|
||||
await click("button", { text: "View Profile" });
|
||||
await contains(".o_form_view");
|
||||
await contains(".o_field_widget[name='name'] .o_input", { value: "Joel" });
|
||||
});
|
||||
|
||||
test("Preserve letter case and accents when creating channel from sidebar", async () => {
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await insertText("input[placeholder='Search a conversation']", "Crème brûlée Fan Club");
|
||||
await click("a", { text: "Create Channel" });
|
||||
await contains(".o-mail-DiscussContent-threadName", { value: "Crème brûlée Fan Club" });
|
||||
});
|
||||
|
||||
test("Create channel must have a name", async () => {
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await click("input[placeholder='Search conversations']");
|
||||
await click("a", { text: "Create Channel" });
|
||||
await click("input[placeholder='Channel name']");
|
||||
await triggerHotkey("Enter");
|
||||
await contains(".invalid-feedback", { text: "Channel must have a name." });
|
||||
});
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
import {
|
||||
SIZES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, disableAnimations, expect, mockPermission, mockTouch, test } from "@odoo/hoot";
|
||||
import {
|
||||
Command,
|
||||
contains as webContains,
|
||||
getService,
|
||||
serverState,
|
||||
swipeLeft,
|
||||
swipeRight,
|
||||
withUser,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("can make DM chat in mobile", async () => {
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Gandalf" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains("button.active", { text: "Notifications" });
|
||||
await click("button", { text: "Chats" });
|
||||
await click(".o-mail-DiscussSearch-inputContainer");
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await insertText("input[placeholder='Search a conversation']", "Gandalf");
|
||||
await contains(".o_command_name", { count: 3 });
|
||||
await click(".o_command_name", { text: "Gandalf" });
|
||||
await contains(".o-mail-ChatWindow", { text: "Gandalf" });
|
||||
});
|
||||
|
||||
test("can search channel in mobile", async () => {
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({ name: "Gryffindors" });
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains("button.active", { text: "Notifications" });
|
||||
await click("button", { text: "Channels" });
|
||||
await click(".o-mail-DiscussSearch-inputContainer");
|
||||
await contains(".o_command_name", { count: 5 });
|
||||
await insertText("input[placeholder='Search a conversation']", "Gryff");
|
||||
await contains(".o_command_name", { count: 3 });
|
||||
await click(".o_command_name", { text: "Gryffindors" });
|
||||
await contains(".o-mail-ChatWindow div[title='Gryffindors']");
|
||||
});
|
||||
|
||||
test("can make new channel in mobile", async () => {
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains("button.active", { text: "Notifications" });
|
||||
await click("button", { text: "Channels" });
|
||||
await click(".o-mail-DiscussSearch-inputContainer");
|
||||
await insertText("input[placeholder='Search a conversation']", "slytherins");
|
||||
await click("a", { text: "Create Channel" });
|
||||
await contains(".o-mail-ChatWindow", { text: "slytherins" });
|
||||
});
|
||||
|
||||
test("new message opens the @ command palette", async () => {
|
||||
await start();
|
||||
await click(".o_menu_systray .dropdown-toggle i[aria-label='Messages']");
|
||||
await click(".o-mail-MessagingMenu button", { text: "New Message" });
|
||||
await contains(".o_command_palette_search .o_namespace", { text: "@" });
|
||||
await contains(".o_command_palette input[placeholder='Search a conversation']");
|
||||
});
|
||||
|
||||
test("channel preview show deleted messages", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "<p>before last</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "<p></p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { text: "before last" });
|
||||
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
|
||||
await contains(".o-mail-NotificationItem-text", {
|
||||
text: "Demo: This message has been removed",
|
||||
});
|
||||
});
|
||||
|
||||
test("deleted message should not show parent message reference and mentions", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "<p>Parent Message</p>",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<p>reply message</p>",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
parent_id: messageId,
|
||||
partner_ids: [serverState.partnerId],
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageInReply", { text: "Parent Message" });
|
||||
await webContains(
|
||||
".o-mail-Message:has(.o-mail-Message-bubble.o-orange):contains('reply message')"
|
||||
).hover();
|
||||
await webContains(
|
||||
".o-mail-Message:has(.o-mail-Message-bubble.o-orange):contains('reply message') [title='Expand']"
|
||||
).click();
|
||||
await click(".o-mail-Message-moreMenu .o-dropdown-item:contains(Delete)");
|
||||
await click(".o_dialog button:contains(Delete)");
|
||||
await contains(".o-mail-Message:not(:has(.o-mail-Message-bubble.o-orange))", {
|
||||
text: "This message has been removed",
|
||||
});
|
||||
await contains(".o-mail-MessageInReply", { count: 0 });
|
||||
});
|
||||
|
||||
test("channel preview ignores transient message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "<p>test</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "/who");
|
||||
await click(".o-mail-Composer button[title='Send']:enabled");
|
||||
await contains(".o_mail_notification", { text: "You are alone in this channel." });
|
||||
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
|
||||
await contains(".o-mail-NotificationItem-text", { text: "Demo: test" });
|
||||
});
|
||||
|
||||
test("channel preview ignores messages from the past", async () => {
|
||||
disableAnimations();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "first message",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
for (let i = 0; i < 100; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: `message ${i}`,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
const newestMessageId = pyEnv["mail.message"].create({
|
||||
body: "last message",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
parent_id: messageId,
|
||||
res_id: channelId,
|
||||
});
|
||||
const [selfMember] = pyEnv["discuss.channel.member"].search_read([
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
["channel_id", "=", channelId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([selfMember.id], {
|
||||
new_message_separator: newestMessageId + 1,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await contains(".o-mail-Message-content", { text: "last message" });
|
||||
await contains(".o-mail-Thread", { scroll: "bottom" });
|
||||
await click(".o-mail-MessageInReply-content", { text: "first message" });
|
||||
await contains(".o-mail-Message", { count: 31 });
|
||||
await contains(".o-mail-Message-content", { text: "first message" });
|
||||
await contains(".o-mail-Message-content", { text: "last message", count: 0 });
|
||||
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
|
||||
await contains(".o-mail-NotificationItem-text", { text: "You: last message" });
|
||||
withUser(serverState.userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "it's a good idea", message_type: "comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-NotificationItem-text", { text: "You: it's a good idea" });
|
||||
});
|
||||
|
||||
test("counter is taking into account non-fetched channels", async () => {
|
||||
mockPermission("notifications", "denied");
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Jane" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_member_ids: [
|
||||
Command.create({ message_unread_counter: 1, partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "first message",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await contains(".o-mail-MessagingMenu-counter", { text: "1" });
|
||||
expect(
|
||||
Boolean(getService("mail.store").Thread.get({ model: "discuss.channel", id: channelId }))
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("counter is updated on receiving message on non-fetched channels", async () => {
|
||||
mockPermission("notifications", "denied");
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Jane" });
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "first message",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await contains(".o_menu_systray .dropdown-toggle i[aria-label='Messages']");
|
||||
await contains(".o-mail-MessagingMenu-counter", { count: 0 });
|
||||
expect(
|
||||
Boolean(getService("mail.store").Thread.get({ model: "discuss.channel", id: channelId }))
|
||||
).toBe(false);
|
||||
withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "good to know", message_type: "comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-MessagingMenu-counter", { text: "1" });
|
||||
});
|
||||
|
||||
test("can use notification item swipe actions", async () => {
|
||||
mockTouch(true);
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", email: "demo@odoo.com" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_type: "chat",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "A message",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains("button.active", { text: "Notifications" });
|
||||
await click("button", { text: "Chats" });
|
||||
await contains(".o-mail-NotificationItem .o-mail-NotificationItem-badge:contains(1)");
|
||||
await swipeRight(".o_actionswiper"); // marks as read
|
||||
await contains(".o-mail-NotificationItem-badge", { count: 0 });
|
||||
await swipeLeft(".o_actionswiper"); // unpins
|
||||
await contains(".o-mail-NotificationItem", { count: 0 });
|
||||
});
|
||||
|
||||
test("counter does not double count channel needaction messages", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Jane" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "Hey @Mitchell Admin",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']"); // fetch channels
|
||||
await contains(".o-mail-NotificationItem", { text: "General" }); // ensure channels fetched
|
||||
await contains(".o-mail-MessagingMenu-counter:text('1')");
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { waitForChannels } from "@bus/../tests/bus_test_helpers";
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { asyncStep, Command, serverState, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("unknown channel can be displayed and interacted with", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Jane" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [Command.create({ partner_id: partnerId })],
|
||||
channel_type: "channel",
|
||||
name: "Not So Secret",
|
||||
});
|
||||
const env = await start();
|
||||
env.services.bus_service.subscribe("discuss.channel/new_message", () =>
|
||||
asyncStep("discuss.channel/new_message")
|
||||
);
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains("button.o-active", { text: "Inbox" });
|
||||
await contains(".o-mail-DiscussSidebarChannel", { count: 0 });
|
||||
await openDiscuss(channelId);
|
||||
await waitForChannels([`discuss.channel_${channelId}`]);
|
||||
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Not So Secret" });
|
||||
await insertText(".o-mail-Composer-input", "Hello", { replace: true });
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message", { text: "Hello" });
|
||||
await waitForSteps(["discuss.channel/new_message"]);
|
||||
await click("button", { text: "Inbox" });
|
||||
await contains(".o-mail-DiscussSidebarChannel:not(.o-active)", { text: "Not So Secret" });
|
||||
await click("[title='Channel Actions']");
|
||||
await click(".o-dropdown-item:contains('Leave Channel')");
|
||||
await click("button", { text: "Leave Conversation" });
|
||||
await contains(".o-mail-DiscussSidebarChannel", { count: 0 });
|
||||
});
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import { AWAY_DELAY } from "@mail/core/common/im_status_service";
|
||||
import { defineMailModels, start, startServer } from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { advanceTime, freezeTime } from "@odoo/hoot-dom";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
asyncStep,
|
||||
makeMockEnv,
|
||||
mockService,
|
||||
patchWithCleanup,
|
||||
restoreRegistry,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
defineMailModels();
|
||||
beforeEach(freezeTime);
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("update presence if IM status changes to offline while this device is online", async () => {
|
||||
mockService("bus_service", { send: (type) => asyncStep(type) });
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
|
||||
await start();
|
||||
await waitForSteps(["update_presence"]);
|
||||
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
|
||||
presence_status: "offline",
|
||||
im_status: "offline",
|
||||
partner_id: serverState.partnerId,
|
||||
});
|
||||
await waitForSteps(["update_presence"]);
|
||||
});
|
||||
|
||||
test("update presence if IM status changes to away while this device is online", async () => {
|
||||
mockService("bus_service", { send: (type) => asyncStep(type) });
|
||||
localStorage.setItem("presence.lastPresence", Date.now());
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
|
||||
await start();
|
||||
await waitForSteps(["update_presence"]);
|
||||
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
|
||||
presence_status: "away",
|
||||
im_status: "away",
|
||||
partner_id: serverState.partnerId,
|
||||
});
|
||||
await waitForSteps(["update_presence"]);
|
||||
});
|
||||
|
||||
test("do not update presence if IM status changes to away while this device is away", async () => {
|
||||
mockService("bus_service", { send: (type) => asyncStep(type) });
|
||||
localStorage.setItem("presence.lastPresence", Date.now() - AWAY_DELAY);
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "away" });
|
||||
await start();
|
||||
await waitForSteps(["update_presence"]);
|
||||
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
|
||||
presence_status: "away",
|
||||
im_status: "away",
|
||||
partner_id: serverState.partnerId,
|
||||
});
|
||||
await waitForSteps([]);
|
||||
});
|
||||
|
||||
test("do not update presence if other user's IM status changes to away", async () => {
|
||||
mockService("bus_service", { send: (type) => asyncStep(type) });
|
||||
localStorage.setItem("presence.lastPresence", Date.now());
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
|
||||
await start();
|
||||
await waitForSteps(["update_presence"]);
|
||||
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
|
||||
presence_status: "away",
|
||||
im_status: "away",
|
||||
partner_id: serverState.publicPartnerId,
|
||||
});
|
||||
await waitForSteps([]);
|
||||
});
|
||||
|
||||
test("update presence when user comes back from away", async () => {
|
||||
mockService("bus_service", {
|
||||
send: (type, payload) => {
|
||||
if (type === "update_presence") {
|
||||
asyncStep(payload.inactivity_period);
|
||||
}
|
||||
},
|
||||
});
|
||||
localStorage.setItem("presence.lastPresence", Date.now() - AWAY_DELAY);
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "away" });
|
||||
await start();
|
||||
await waitForSteps([AWAY_DELAY]);
|
||||
localStorage.setItem("presence.lastPresence", Date.now());
|
||||
await waitForSteps([0]);
|
||||
});
|
||||
|
||||
test("update presence when user status changes to away", async () => {
|
||||
mockService("bus_service", {
|
||||
send: (type, payload) => {
|
||||
if (type === "update_presence") {
|
||||
asyncStep(payload.inactivity_period);
|
||||
}
|
||||
},
|
||||
});
|
||||
localStorage.setItem("presence.lastPresence", Date.now());
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
|
||||
await start();
|
||||
await waitForSteps([0]);
|
||||
await advanceTime(AWAY_DELAY);
|
||||
await waitForSteps([AWAY_DELAY]);
|
||||
});
|
||||
|
||||
test("new tab update presence when user comes back from away", async () => {
|
||||
// Tabs notify presence with a debounced update, and the status service skips
|
||||
// duplicates. This test ensures a new tab that never sent presence still issues
|
||||
// its first update (important when old tabs close and new ones replace them).
|
||||
localStorage.setItem("presence.lastPresence", Date.now() - AWAY_DELAY);
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "offline" });
|
||||
const tabEnv_1 = await makeMockEnv();
|
||||
patchWithCleanup(tabEnv_1.services.bus_service, {
|
||||
send: (type) => {
|
||||
if (type === "update_presence") {
|
||||
expect.step("update_presence");
|
||||
}
|
||||
},
|
||||
});
|
||||
tabEnv_1.services.bus_service.start();
|
||||
await expect.waitForSteps(["update_presence"]);
|
||||
restoreRegistry(registry);
|
||||
const tabEnv_2 = await makeMockEnv(null, { makeNew: true });
|
||||
patchWithCleanup(tabEnv_2.services.bus_service, {
|
||||
send: (type) => {
|
||||
if (type === "update_presence") {
|
||||
expect.step("update_presence");
|
||||
}
|
||||
},
|
||||
});
|
||||
tabEnv_2.services.bus_service.start();
|
||||
await expect.waitForSteps([]);
|
||||
localStorage.setItem("presence.lastPresence", Date.now()); // Simulate user presence.
|
||||
await expect.waitForSteps(["update_presence", "update_presence"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test, expect } from "@odoo/hoot";
|
||||
import { disableAnimations } from "@odoo/hoot-mock";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
async function assertPinnedPanelUnpinCount(expectedCount) {
|
||||
await contains(".dropdown-item", { text: "Unpin", count: expectedCount });
|
||||
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
|
||||
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message", {
|
||||
text: "Test pinned message",
|
||||
});
|
||||
expect(".o-discuss-PinnedMessagesPanel button[title='Unpin']").toHaveCount(expectedCount);
|
||||
}
|
||||
|
||||
test("Pin message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello world!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
|
||||
await contains(".o-discuss-PinnedMessagesPanel p", {
|
||||
text: "This channel doesn't have any pinned messages.",
|
||||
});
|
||||
await click(".o-mail-Message [title='Expand']");
|
||||
await click(".dropdown-item", { text: "Pin" });
|
||||
await click(".modal-footer button", { text: "Yeah, pin it!" });
|
||||
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message", { text: "Hello world!" });
|
||||
});
|
||||
|
||||
test("Unpin message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello world!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
pinned_at: "2023-03-30 11:27:11",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
|
||||
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message");
|
||||
await click(".o-mail-Message [title='Expand']");
|
||||
await click(".dropdown-item", { text: "Unpin" });
|
||||
await click(".modal-footer button", { text: "Yes, remove it please" });
|
||||
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message", { count: 0 });
|
||||
});
|
||||
|
||||
test("Open pinned panel from notification", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello world!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(":nth-child(1 of .o-mail-Message) [title='Expand']");
|
||||
await click(".dropdown-item", { text: "Pin" });
|
||||
await click(".modal-footer button", { text: "Yeah, pin it!" });
|
||||
await contains(".o-discuss-PinnedMessagesPanel", { count: 0 });
|
||||
await click(".o_mail_notification a", { text: "See all pinned messages" });
|
||||
await contains(".o-discuss-PinnedMessagesPanel");
|
||||
});
|
||||
|
||||
test("Jump to message", async () => {
|
||||
disableAnimations();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello world!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
pinned_at: "2023-04-03 08:15:04",
|
||||
});
|
||||
for (let i = 0; i < 20; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Non Empty Body ".repeat(25),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
|
||||
await click(".o-discuss-PinnedMessagesPanel a[role='button']", { text: "Jump" });
|
||||
await contains(".o-mail-Thread .o-mail-Message-body", { text: "Hello world!", visible: true });
|
||||
});
|
||||
|
||||
test("Jump to message from notification", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello world!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
for (let i = 0; i < 20; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Non Empty Body ".repeat(25),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 21 });
|
||||
await click(":nth-child(1 of .o-mail-Message) [title='Expand']");
|
||||
await click(".dropdown-item", { text: "Pin" });
|
||||
await click(".modal-footer button", { text: "Yeah, pin it!" });
|
||||
await contains(".o_mail_notification");
|
||||
await scroll(".o-mail-Thread", "bottom");
|
||||
await contains(".o-mail-Thread", { scroll: "bottom" });
|
||||
await click(".o_mail_notification a", { text: "message" });
|
||||
await contains(".o-mail-Thread", { count: 0, scroll: "bottom" });
|
||||
});
|
||||
|
||||
test("can add reactions from pinned panel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Hello world!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
pinned_at: "2025-10-09 11:15:04",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Message-actions [title='Add a Reaction']");
|
||||
await click(".o-mail-QuickReactionMenu button", { text: "👍" });
|
||||
await contains(".o-mail-MessageReaction", { text: "👍1" });
|
||||
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
|
||||
await click(".o-discuss-PinnedMessagesPanel .o-mail-Message [title='Add a Reaction']");
|
||||
await click(".o-mail-QuickReactionMenu button", { text: "👍" });
|
||||
await contains(".o-mail-MessageReaction", { count: 0 });
|
||||
});
|
||||
|
||||
test("Guest user cannot see unpin button", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "General",
|
||||
channel_type: "channel",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Test pinned message",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
pinned_at: "2023-03-30 11:27:11",
|
||||
});
|
||||
await start({ authenticateAs: false });
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { text: "Test pinned message" });
|
||||
expect(".o-mail-Message [title='Expand']").toHaveCount(0);
|
||||
await assertPinnedPanelUnpinCount(0);
|
||||
});
|
||||
|
||||
test("Internal user can see unpin button", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Test pinned message",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
pinned_at: "2023-03-30 11:27:11",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Message [title='Expand']");
|
||||
await assertPinnedPanelUnpinCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
patchUiSize,
|
||||
scroll,
|
||||
SIZES,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { expect, mockTouch, mockUserAgent, test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { tick } from "@odoo/hoot-mock";
|
||||
import { serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { HIGHLIGHT_CLASS } from "@mail/core/common/message_search_hook";
|
||||
|
||||
defineMailModels();
|
||||
|
||||
test.tags("desktop");
|
||||
test("Should have a search button", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains("[title='Search Messages']");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Should open the search panel when search button is clicked", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click("[title='Search Messages']");
|
||||
await contains(".o-mail-SearchMessagesPanel");
|
||||
await contains(".o_searchview");
|
||||
await contains(".o_searchview_input");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Should open the search panel with hotkey 'f'", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await press("alt+f");
|
||||
await contains(".o-mail-SearchMessagesPanel");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search a message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await click("button[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search should be hightlighted", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(`.o-mail-SearchMessagesPanel .o-mail-Message .${HIGHLIGHT_CLASS}`);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search a starred message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
starred_partner_ids: [serverState.partnerId],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_starred");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search a message in inbox", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search a message in history (desktop)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: false,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
is_read: true,
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_history");
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
|
||||
await click(".o-mail-SearchMessagesPanel .o-mail-MessageCard-jump");
|
||||
await contains(".o-mail-Thread .o-mail-Message.o-highlighted");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("Search a message in history (mobile)", async () => {
|
||||
mockTouch(true);
|
||||
mockUserAgent("android");
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: false,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
is_read: true,
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_history");
|
||||
await contains(".o-mail-Thread");
|
||||
await click("[title='Search Messages']");
|
||||
await contains(".o-mail-SearchMessagesPanel");
|
||||
await contains(".o-mail-Thread", { count: 0 });
|
||||
await insertText(".o_searchview_input", "message");
|
||||
await triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
|
||||
await click(".o-mail-MessageCard-jump");
|
||||
await contains(".o-mail-Thread");
|
||||
await contains(".o-mail-SearchMessagesPanel", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Should close the search panel when search button is clicked again", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click("[title='Search Messages']");
|
||||
await click("[title='Close Search']");
|
||||
await contains(".o-mail-SearchMessagesPanel");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search a message in 60 messages should return 30 message first", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
for (let i = 0; i < 60; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 30 });
|
||||
// give enough time to useVisible to potentially load more (unexpected) messages
|
||||
await tick();
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 30 });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Scrolling to the bottom should load more searched message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
for (let i = 0; i < 90; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 30 });
|
||||
await scroll(".o-mail-SearchMessagesPanel .o-mail-ActionPanel", "bottom");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 60 });
|
||||
// give enough time to useVisible to potentially load more (unexpected) messages
|
||||
await tick();
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 60 });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Editing the searched term should not edit the current searched term", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
for (let i = 0; i < 60; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a message",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
onRpcBefore("/discuss/channel/messages", (args) => {
|
||||
if (args.search_term) {
|
||||
const { search_term } = args;
|
||||
expect(search_term).toBe("message");
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
|
||||
await click("[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "message");
|
||||
triggerHotkey("Enter");
|
||||
await insertText(".o_searchview_input", "test");
|
||||
await scroll(".o-mail-SearchMessagesPanel .o-mail-ActionPanel", "bottom");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search a message containing round brackets", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "This is a (message)",
|
||||
attachment_ids: [],
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message");
|
||||
await click("button[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "(message");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Search a message containing single quotes", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "I can't do it");
|
||||
await click(".o-sendMessageActive:enabled");
|
||||
await contains(".o-mail-Message");
|
||||
await click("button[title='Search Messages']");
|
||||
await insertText(".o_searchview_input", "can't");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
|
||||
});
|
||||
|
|
@ -0,0 +1,906 @@
|
|||
import { insertText as htmlInsertText } from "@html_editor/../tests/_helpers/user_actions";
|
||||
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { advanceTime, mockDate } from "@odoo/hoot-mock";
|
||||
import {
|
||||
asyncStep,
|
||||
Command,
|
||||
getService,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
withUser,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Store } from "@mail/core/common/store_service";
|
||||
import { LONG_TYPING, SHORT_TYPING } from "@mail/discuss/typing/common/composer_patch";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test('[text composer] receive other member typing status "is typing"', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
// simulate receive typing notification from demo
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test('receive other member typing status "is typing"', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
});
|
||||
|
||||
test('[text composer] receive other member typing status "is typing" then "no longer is typing"', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
// simulate receive typing notification from demo "is typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
// simulate receive typing notification from demo "is no longer typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test('receive other member typing status "is typing" then "no longer is typing"', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
});
|
||||
|
||||
test('[text composer] assume other member typing status becomes "no longer is typing" after long without any updated typing status', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
// simulate receive typing notification from demo "is typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
await advanceTime(Store.OTHER_LONG_TYPING);
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test('assume other member typing status becomes "no longer is typing" after long without any updated typing status', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
await advanceTime(Store.OTHER_LONG_TYPING);
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
});
|
||||
|
||||
test('"is typing" timeout should work even when 2 notify_typing happen at the exact same time', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
mockDate("2024-01-01 12:00:00");
|
||||
await withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
await advanceTime(Store.OTHER_LONG_TYPING);
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing..." });
|
||||
});
|
||||
|
||||
test('[text composer] other member typing status "is typing" refreshes of assuming no longer typing', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
// simulate receive typing notification from demo "is typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
// simulate receive typing notification from demo "is typing" again after long time.
|
||||
await advanceTime(LONG_TYPING);
|
||||
await withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await advanceTime(LONG_TYPING);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
await advanceTime(Store.OTHER_LONG_TYPING - LONG_TYPING);
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test('other member typing status "is typing" refreshes of assuming no longer typing', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
await advanceTime(LONG_TYPING);
|
||||
await withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await advanceTime(LONG_TYPING);
|
||||
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
|
||||
await advanceTime(Store.OTHER_LONG_TYPING - LONG_TYPING);
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
});
|
||||
|
||||
test('[text composer] receive several other members typing status "is typing"', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [userId_1, userId_2, userId_3] = pyEnv["res.users"].create([
|
||||
{ name: "Other 10" },
|
||||
{ name: "Other 11" },
|
||||
{ name: "Other 12" },
|
||||
]);
|
||||
const [partnerId_1, partnerId_2, partnerId_3] = pyEnv["res.partner"].create([
|
||||
{ name: "Other 10", user_ids: [userId_1] },
|
||||
{ name: "Other 11", user_ids: [userId_2] },
|
||||
{ name: "Other 12", user_ids: [userId_3] },
|
||||
]);
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
Command.create({ partner_id: partnerId_3 }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
// simulate receive typing notification from other 10 (is typing)
|
||||
withUser(userId_1, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 10 is typing..." });
|
||||
// simulate receive typing notification from other 11 (is typing)
|
||||
withUser(userId_2, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 10 and Other 11 are typing..." });
|
||||
// simulate receive typing notification from other 12 (is typing)
|
||||
withUser(userId_3, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 10, Other 11 and more are typing..." });
|
||||
// simulate receive typing notification from other 10 (no longer is typing)
|
||||
withUser(userId_1, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 11 and Other 12 are typing..." });
|
||||
// simulate receive typing notification from other 10 (is typing again)
|
||||
withUser(userId_1, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 11, Other 12 and more are typing..." });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test('receive several other members typing status "is typing"', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [userId_1, userId_2, userId_3] = pyEnv["res.users"].create([
|
||||
{ name: "Other 10" },
|
||||
{ name: "Other 11" },
|
||||
{ name: "Other 12" },
|
||||
]);
|
||||
const [partnerId_1, partnerId_2, partnerId_3] = pyEnv["res.partner"].create([
|
||||
{ name: "Other 10", user_ids: [userId_1] },
|
||||
{ name: "Other 11", user_ids: [userId_2] },
|
||||
{ name: "Other 12", user_ids: [userId_3] },
|
||||
]);
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId_1 }),
|
||||
Command.create({ partner_id: partnerId_2 }),
|
||||
Command.create({ partner_id: partnerId_3 }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
withUser(userId_1, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 10 is typing..." });
|
||||
withUser(userId_2, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 10 and Other 11 are typing..." });
|
||||
withUser(userId_3, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 10, Other 11 and more are typing..." });
|
||||
withUser(userId_1, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 11 and Other 12 are typing..." });
|
||||
withUser(userId_1, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing", { text: "Other 11, Other 12 and more are typing..." });
|
||||
});
|
||||
|
||||
test("[text composer] current partner notify is typing to other thread members", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) => {
|
||||
if (!testEnded) {
|
||||
asyncStep(`notify_typing:${args.is_typing}`);
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("current partner notify is typing to other thread members", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) => {
|
||||
if (!testEnded) {
|
||||
asyncStep(`notify_typing:${args.is_typing}`);
|
||||
}
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test("[text composer] current partner notify is typing again to other members for long continuous typing", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) => {
|
||||
if (!testEnded) {
|
||||
asyncStep(`notify_typing:${args.is_typing}`);
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
await insertText(".o-mail-Composer-input", "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
// simulate current partner typing a character for a long time.
|
||||
const elapseTickTime = SHORT_TYPING / 2;
|
||||
for (let i = 0; i <= LONG_TYPING / elapseTickTime; i++) {
|
||||
await insertText(".o-mail-Composer-input", "a");
|
||||
await advanceTime(elapseTickTime);
|
||||
}
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("current partner notify is typing again to other members for long continuous typing", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) => {
|
||||
if (!testEnded) {
|
||||
asyncStep(`notify_typing:${args.is_typing}`);
|
||||
}
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable");
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
const elapseTickTime = SHORT_TYPING / 2;
|
||||
for (let i = 0; i <= LONG_TYPING / elapseTickTime; i++) {
|
||||
await htmlInsertText(editor, "a");
|
||||
await advanceTime(elapseTickTime);
|
||||
}
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test("[text composer] current partner notify no longer is typing to thread members after 5 seconds inactivity", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) =>
|
||||
asyncStep(`notify_typing:${args.is_typing}`)
|
||||
);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
|
||||
await insertText(".o-mail-Composer-input", "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
await advanceTime(SHORT_TYPING);
|
||||
await waitForSteps(["notify_typing:false"]);
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("current partner notify no longer is typing to thread members after 5 seconds inactivity", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) =>
|
||||
asyncStep(`notify_typing:${args.is_typing}`)
|
||||
);
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
await advanceTime(SHORT_TYPING);
|
||||
await waitForSteps(["notify_typing:false"]);
|
||||
});
|
||||
|
||||
test("[text composer] current partner is typing should not translate on textual typing status", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) => {
|
||||
if (!testEnded) {
|
||||
asyncStep(`notify_typing:${args.is_typing}`);
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("current partner is typing should not translate on textual typing status", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
let testEnded = false;
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) => {
|
||||
if (!testEnded) {
|
||||
asyncStep(`notify_typing:${args.is_typing}`);
|
||||
}
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
await contains(".o-discuss-Typing");
|
||||
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
|
||||
testEnded = true;
|
||||
});
|
||||
|
||||
test("[text composer] chat: correspondent is typing", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
im_status: "online",
|
||||
name: "Demo",
|
||||
user_ids: [userId],
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-DiscussSidebarChannel .o-mail-DiscussSidebarChannel-threadIcon");
|
||||
await contains(".fa-circle.text-success");
|
||||
// simulate receive typing notification from demo "is typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing-icon[title='Demo is typing...']");
|
||||
// simulate receive typing notification from demo "no longer is typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains(".fa-circle.text-success");
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("chat: correspondent is typing", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
im_status: "online",
|
||||
name: "Demo",
|
||||
user_ids: [userId],
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-DiscussSidebarChannel .o-mail-DiscussSidebarChannel-threadIcon");
|
||||
await contains(".fa-circle.text-success");
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-Typing-icon[title='Demo is typing...']");
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains(".fa-circle.text-success");
|
||||
});
|
||||
|
||||
test("[text composer] chat: correspondent is typing in chat window", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
im_status: "online",
|
||||
name: "Demo",
|
||||
user_ids: [userId],
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem");
|
||||
await contains("[title='Demo is typing...']", { count: 0 });
|
||||
// simulate receive typing notification from demo "is typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains("[title='Demo is typing...']", { count: 2 }); // icon in header & text above composer
|
||||
// simulate receive typing notification from demo "no longer is typing"
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains("[title='Demo is typing...']", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("chat: correspondent is typing in chat window", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
im_status: "online",
|
||||
name: "Demo",
|
||||
user_ids: [userId],
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem");
|
||||
await contains("[title='Demo is typing...']", { count: 0 });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains("[title='Demo is typing...']", { count: 2 });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: false,
|
||||
})
|
||||
);
|
||||
await contains("[title='Demo is typing...']", { count: 0 });
|
||||
});
|
||||
|
||||
test("[text composer] show typing in member list", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Other 10" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Other 10", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMember", { count: 2 });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-ChannelMemberList [title='Other 10 is typing...']");
|
||||
withUser(serverState.userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(
|
||||
`.o-discuss-ChannelMemberList [title='${serverState.partnerName} is typing...']`
|
||||
);
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("show typing in member list", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Other 10" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Other 10", user_ids: [userId] });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "channel",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-discuss-ChannelMember", { count: 2 });
|
||||
withUser(userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(".o-discuss-ChannelMemberList [title='Other 10 is typing...']");
|
||||
withUser(serverState.userId, () =>
|
||||
rpc("/discuss/channel/notify_typing", {
|
||||
channel_id: channelId,
|
||||
is_typing: true,
|
||||
})
|
||||
);
|
||||
await contains(
|
||||
`.o-discuss-ChannelMemberList [title='${serverState.partnerName} is typing...']`
|
||||
);
|
||||
});
|
||||
|
||||
test("[text composer] switching to another channel triggers notify_typing to stop", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
im_status: "online",
|
||||
name: "Demo",
|
||||
user_ids: [userId],
|
||||
});
|
||||
const chatId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["discuss.channel"].create({ name: "general" });
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) =>
|
||||
asyncStep(`notify_typing:${args.is_typing}`)
|
||||
);
|
||||
await start();
|
||||
await openDiscuss(chatId);
|
||||
await insertText(".o-mail-Composer-input", "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "general" });
|
||||
await advanceTime(SHORT_TYPING / 2);
|
||||
await waitForSteps(["notify_typing:false"]);
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("switching to another channel triggers notify_typing to stop", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const userId = pyEnv["res.users"].create({ name: "Demo" });
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
im_status: "online",
|
||||
name: "Demo",
|
||||
user_ids: [userId],
|
||||
});
|
||||
const chatId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["discuss.channel"].create({ name: "general" });
|
||||
onRpcBefore("/discuss/channel/notify_typing", (args) =>
|
||||
asyncStep(`notify_typing:${args.is_typing}`)
|
||||
);
|
||||
await start();
|
||||
const composerService = getService("mail.composer");
|
||||
composerService.setHtmlComposer();
|
||||
await openDiscuss(chatId);
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable");
|
||||
const editor = {
|
||||
document,
|
||||
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "a");
|
||||
await waitForSteps(["notify_typing:true"]);
|
||||
await click(".o-mail-DiscussSidebar-item", { text: "general" });
|
||||
await advanceTime(SHORT_TYPING / 2);
|
||||
await waitForSteps(["notify_typing:false"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
mockGetMedia,
|
||||
openDiscuss,
|
||||
patchVoiceMessageAudio,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, globals, test } from "@odoo/hoot";
|
||||
import { Deferred, mockDate } from "@odoo/hoot-mock";
|
||||
import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { loadLamejs } from "@mail/discuss/voice_message/common/voice_message_service";
|
||||
import { VoicePlayer } from "@mail/discuss/voice_message/common/voice_player";
|
||||
import { patchable } from "@mail/discuss/voice_message/common/voice_recorder";
|
||||
import { Mp3Encoder } from "@mail/discuss/voice_message/common/mp3_encoder";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("make voice message in chat", async () => {
|
||||
const file = new File([new Uint8Array(25000)], "test.mp3", { type: "audio/mp3" });
|
||||
const voicePlayerDrawing = new Deferred();
|
||||
patchWithCleanup(Mp3Encoder.prototype, {
|
||||
encode() {},
|
||||
finish() {
|
||||
return Array(500).map(() => new Int8Array());
|
||||
},
|
||||
});
|
||||
patchWithCleanup(patchable, { makeFile: () => file });
|
||||
patchWithCleanup(VoicePlayer.prototype, {
|
||||
async drawWave(...args) {
|
||||
voicePlayerDrawing.resolve();
|
||||
return super.drawWave(...args);
|
||||
},
|
||||
async fetchFile() {
|
||||
return super.fetchFile("/mail/static/src/audio/call-invitation.mp3");
|
||||
},
|
||||
_fetch(url) {
|
||||
if (url.includes("call-invitation.mp3")) {
|
||||
const realFetch = globals.fetch;
|
||||
return realFetch(...arguments);
|
||||
}
|
||||
return super._fetch(...arguments);
|
||||
},
|
||||
});
|
||||
mockGetMedia();
|
||||
const resources = patchVoiceMessageAudio();
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await loadLamejs(); // simulated AudioProcess.process() requires lamejs fully loaded
|
||||
await click(".o-mail-Composer button[title='More Actions']");
|
||||
await contains(".dropdown-item:contains('Voice Message')");
|
||||
mockDate("2023-07-31 13:00:00");
|
||||
await click(".dropdown-item:contains('Voice Message')");
|
||||
await contains(".o-mail-VoiceRecorder", { text: "00 : 00" });
|
||||
/**
|
||||
* Simulate 10 sec elapsed.
|
||||
* `patchDate` does not freeze the time, it merely changes the value of "now" at the time it was
|
||||
* called. The code of click following the first `patchDate` doesn't actually happen at the time
|
||||
* that was specified, but few miliseconds later (8 ms on my machine).
|
||||
* The process following the next `patchDate` is intended to be between 10s and 11s later than
|
||||
* the click, because the test wants to assert a 10 sec counter, and the two dates are
|
||||
* substracted and then rounded down in the code (it means absolute values are irrelevant here).
|
||||
* The problem with aiming too close to a 10s difference is that if the click is longer than
|
||||
* the following process, it will round down to 9s.
|
||||
* The problem with aiming too close to a 11s difference is that if the click is shorter than
|
||||
* the following process, it will round down to 11s.
|
||||
* The best bet is therefore to use 10s + 500ms difference.
|
||||
*/
|
||||
mockDate("2023-07-31 13:00:10.500");
|
||||
// simulate some microphone data
|
||||
resources.audioProcessor.process([[new Float32Array(128)]]);
|
||||
await contains(".o-mail-VoiceRecorder", { text: "00 : 10" });
|
||||
await click(".o-mail-Composer button[title='Stop Recording']");
|
||||
await contains(".o-mail-VoicePlayer");
|
||||
// wait for audio stream decode + drawing of waves
|
||||
await voicePlayerDrawing;
|
||||
await contains(".o-mail-VoicePlayer button[title='Play']");
|
||||
await contains(".o-mail-VoicePlayer canvas", { count: 2 }); // 1 for global waveforms, 1 for played waveforms
|
||||
await contains(".o-mail-VoicePlayer", { text: "00 : 03" }); // duration of call-invitation_.mp3
|
||||
await click(".o-mail-Composer button[title='More Actions']");
|
||||
await contains(".dropdown-item:contains('Attach Files')"); // check menu loaded
|
||||
await contains(".dropdown-item:contains('Voice Message')", { count: 0 }); // only 1 voice message at a time
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { addBusServiceListeners, lockWebsocketConnect } from "@bus/../tests/bus_test_helpers";
|
||||
import { getWebSocketWorker } from "@bus/../tests/mock_websocket";
|
||||
import { WEBSOCKET_CLOSE_CODES } from "@bus/workers/websocket_worker";
|
||||
import { defineMailModels, openDiscuss, start } from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, runAllTimers, waitFor, waitForNone } from "@odoo/hoot-dom";
|
||||
|
||||
import {
|
||||
asyncStep,
|
||||
makeMockServer,
|
||||
MockServer,
|
||||
patchWithCleanup,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
defineMailModels();
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("show warning when bus connection encounters issues", async () => {
|
||||
await makeMockServer();
|
||||
// Avoid excessively long exponential backoff.
|
||||
patchWithCleanup(getWebSocketWorker(), {
|
||||
INITIAL_RECONNECT_DELAY: 50,
|
||||
RECONNECT_JITTER: 50,
|
||||
});
|
||||
// The bus service listens to online/offline events. Prevent them to make the
|
||||
// test deterministic.
|
||||
for (const event of ["online", "offline"]) {
|
||||
browser.addEventListener(
|
||||
event,
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
}
|
||||
addBusServiceListeners(
|
||||
["BUS:CONNECT", () => asyncStep("BUS:CONNECT")],
|
||||
["BUS:RECONNECT", () => asyncStep("BUS:RECONNECT")],
|
||||
["BUS:RECONNECTING", () => asyncStep("BUS:RECONNECTING")]
|
||||
);
|
||||
await start();
|
||||
await openDiscuss();
|
||||
await waitForSteps(["BUS:CONNECT"]);
|
||||
const unlockWebsocket = lockWebsocketConnect();
|
||||
MockServer.env["bus.bus"]._simulateDisconnection(WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE);
|
||||
await waitForSteps(["BUS:RECONNECTING"]);
|
||||
expect(await waitFor(".o-bus-ConnectionAlert", { timeout: 2500 })).toHaveText(
|
||||
"Real-time connection lost..."
|
||||
);
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o-bus-ConnectionAlert").toHaveText("Real-time connection lost...");
|
||||
unlockWebsocket();
|
||||
await runAllTimers();
|
||||
await waitForSteps(["BUS:RECONNECT"]);
|
||||
await waitForNone(".o-bus-ConnectionAlert");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,108 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { Store } from "@mail/core/common/store_service";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("initially online", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "online" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-DiscussContent-header .o-mail-ImStatus i[title='Online']");
|
||||
});
|
||||
|
||||
test("initially offline", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "offline" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-DiscussContent-header .o-mail-ImStatus i[title='Offline']");
|
||||
});
|
||||
|
||||
test("initially away", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "away" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-DiscussContent-header .o-mail-ImStatus i[title='Idle']");
|
||||
});
|
||||
|
||||
test("change icon on change partner im_status", async () => {
|
||||
patchWithCleanup(Store, { IM_STATUS_DEBOUNCE_DELAY: 0 });
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ channel_type: "chat" });
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "online" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-DiscussContent-header .o-mail-ImStatus i[title='Online']");
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "offline" });
|
||||
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
|
||||
partner_id: serverState.partnerId,
|
||||
im_status: "offline",
|
||||
presence_status: "offline",
|
||||
});
|
||||
await contains(".o-mail-DiscussContent-header .o-mail-ImStatus i[title='Offline']");
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "away" });
|
||||
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
|
||||
partner_id: serverState.partnerId,
|
||||
im_status: "away",
|
||||
presence_status: "away",
|
||||
});
|
||||
await contains(".o-mail-DiscussContent-header .o-mail-ImStatus i[title='Idle']");
|
||||
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "online" });
|
||||
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
|
||||
partner_id: serverState.partnerId,
|
||||
im_status: "online",
|
||||
presence_status: "online",
|
||||
});
|
||||
await contains(".o-mail-DiscussContent-header .o-mail-ImStatus i[title='Online']");
|
||||
});
|
||||
|
||||
test("show im status in messaging menu preview of chat", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "online" });
|
||||
pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem", {
|
||||
text: "Demo",
|
||||
contains: ["i[aria-label='User is online']"],
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,762 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
import {
|
||||
asyncStep,
|
||||
mockService,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
withUser,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("reply: discard on reply button toggle", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Reply')");
|
||||
await contains(".o-mail-Composer");
|
||||
await click("[title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Reply')");
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("reply: discard on pressing escape", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({
|
||||
email: "testpartnert@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Reply')");
|
||||
await contains(".o-mail-Composer");
|
||||
// Escape on emoji picker does not stop replying
|
||||
await click(".o-mail-Composer button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker");
|
||||
triggerHotkey("Escape");
|
||||
await contains(".o-EmojiPicker", { count: 0 });
|
||||
await contains(".o-mail-Composer");
|
||||
// Escape on suggestion prompt does not stop replying
|
||||
await insertText(".o-mail-Composer-input", "@");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open");
|
||||
triggerHotkey("Escape");
|
||||
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
|
||||
await contains(".o-mail-Composer");
|
||||
await click(".o-mail-Composer-input").catch(() => {});
|
||||
await contains(".o-mail-Composer.o-focused");
|
||||
triggerHotkey("Escape");
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
});
|
||||
|
||||
test('"reply to" composer should log note if message replied to is a note', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
subtype_id: pyEnv["mail.message.subtype"].search([
|
||||
["subtype_xmlid", "=", "mail.mt_note"],
|
||||
])[0],
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
onRpcBefore("/mail/message/post", (args) => {
|
||||
asyncStep("/mail/message/post");
|
||||
expect(args.post_data.message_type).toBe("comment");
|
||||
expect(args.post_data.subtype_xmlid).toBe("mail.mt_note");
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Reply')");
|
||||
await contains(".o-mail-Composer [placeholder='Log an internal note…']");
|
||||
await insertText(".o-mail-Composer-input", "Test");
|
||||
await click(".o-mail-Composer button[title='Log']");
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
await waitForSteps(["/mail/message/post"]);
|
||||
});
|
||||
|
||||
test('"reply to" composer should send message if message replied to is not a note', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
subtype_id: pyEnv["mail.message.subtype"].search([
|
||||
["subtype_xmlid", "=", "mail.mt_comment"],
|
||||
])[0],
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
onRpcBefore("/mail/message/post", (args) => {
|
||||
asyncStep("/mail/message/post");
|
||||
expect(args.post_data.message_type).toBe("comment");
|
||||
expect(args.post_data.subtype_xmlid).toBe("mail.mt_comment");
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Reply')");
|
||||
await contains(".o-mail-Composer [placeholder='Send a message to followers…']");
|
||||
await insertText(".o-mail-Composer-input", "Test");
|
||||
await click(".o-mail-Composer button[title='Send']:enabled");
|
||||
await contains(".o-mail-Composer button[title='Send']", { count: 0 });
|
||||
await waitForSteps(["/mail/message/post"]);
|
||||
});
|
||||
|
||||
test("show subject of message in Inbox", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message", { text: "Subject: Salutations, voyageurnot empty" });
|
||||
});
|
||||
|
||||
test("show subject of message in history", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
is_read: true,
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_history");
|
||||
await contains(".o-mail-Message", { text: "Subject: Salutations, voyageurnot empty" });
|
||||
});
|
||||
|
||||
test("subject should not be shown when subject is the same as the thread name", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Salutations, voyageur" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("subject should not be shown when subject is the same as the thread name and both have the same prefix", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Re: Salutations, voyageur" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
subject: "Re: Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test('subject should not be shown when subject differs from thread name only by the "Re:" prefix', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Salutations, voyageur" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
subject: "Re: Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test('subject should not be shown when subject differs from thread name only by the "Fw:" and "Re:" prefix', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Salutations, voyageur" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
subject: "Fw: Re: Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("subject should be shown when the thread name has an extra prefix compared to subject", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Re: Salutations, voyageur" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
subject: "Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test('subject should not be shown when subject differs from thread name only by the "fw:" prefix and both contain another common prefix', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Re: Salutations, voyageur" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
subject: "fw: re: Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test('subject should not be shown when subject differs from thread name only by the "Re: Re:" prefix', async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Salutations, voyageur" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
needaction: true,
|
||||
subject: "Re: Re: Salutations, voyageur",
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message", {
|
||||
count: 0,
|
||||
text: "Subject: Salutations, voyageurnot empty",
|
||||
});
|
||||
});
|
||||
|
||||
test("inbox: mark all messages as read", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const [messageId_1, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
},
|
||||
{
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { text: "2" }] });
|
||||
await contains(".o-mail-DiscussSidebarChannel", {
|
||||
contains: [
|
||||
["span", { text: "General" }],
|
||||
[".badge", { text: "2" }],
|
||||
],
|
||||
});
|
||||
await contains(".o-mail-DiscussContent .o-mail-Message", { count: 2 });
|
||||
await click(".o-mail-DiscussContent-header button:enabled", { text: "Mark all read" });
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { count: 0 }] });
|
||||
await contains(".o-mail-DiscussSidebarChannel", {
|
||||
contains: [
|
||||
["span", { text: "General" }],
|
||||
[".badge", { count: 0 }],
|
||||
],
|
||||
});
|
||||
await contains(".o-mail-Message", { count: 0 });
|
||||
await contains("button:disabled", { text: "Mark all read" });
|
||||
});
|
||||
|
||||
test("inbox: mark as read should not display jump to present", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const msgIds = pyEnv["mail.message"].create(
|
||||
Array(30)
|
||||
.keys()
|
||||
.map((i) => ({
|
||||
body: "not empty".repeat(100),
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
}))
|
||||
);
|
||||
pyEnv["mail.notification"].create(
|
||||
Array(30)
|
||||
.keys()
|
||||
.map((i) => ({
|
||||
mail_message_id: msgIds[i],
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
}))
|
||||
);
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
// scroll up so that there's the "Jump to Present".
|
||||
// So that assertion of negative matches the positive assertion
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await scroll(".o-mail-Thread", 0);
|
||||
await contains("[title='Jump to Present']");
|
||||
await click(".o-mail-DiscussContent-header button:enabled", { text: "Mark all read" });
|
||||
await contains("[title='Jump to Present']", { count: 0 });
|
||||
});
|
||||
|
||||
test("click on (non-channel/non-partner) origin thread link should redirect to form view", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.fake"].create({ name: "Some record" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.fake",
|
||||
needaction: true,
|
||||
res_id: fakeId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
const def = new Deferred();
|
||||
mockService("action", {
|
||||
async doAction(action) {
|
||||
if (action?.res_model !== "res.fake") {
|
||||
return super.doAction(...arguments);
|
||||
}
|
||||
// Callback of doing an action (action manager).
|
||||
// Expected to be called on click on origin thread link,
|
||||
// which redirects to form view of record related to origin thread
|
||||
asyncStep("do-action");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.views).toEqual([[false, "form"]]);
|
||||
expect(action.res_model).toBe("res.fake");
|
||||
expect(action.res_id).toBe(fakeId);
|
||||
def.resolve();
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click(".o-mail-Message-header a", { text: "Some record" });
|
||||
await def;
|
||||
await waitForSteps(["do-action"]);
|
||||
});
|
||||
|
||||
test("inbox messages are never squashed", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
const [messageId_1, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
author_id: partnerId,
|
||||
body: "<p>body1</p>",
|
||||
date: "2019-04-20 10:00:00",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
},
|
||||
{
|
||||
author_id: partnerId,
|
||||
body: "<p>body2</p>",
|
||||
date: "2019-04-20 10:00:30",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
await contains(".o-mail-Message:not(.o-squashed)", { text: "body1" });
|
||||
await contains(".o-mail-Message:not(.o-squashed)", { text: "body2" });
|
||||
await click(".o-mail-DiscussSidebarChannel", { text: "test" });
|
||||
await contains(".o-mail-Message.o-squashed", { text: "body2" });
|
||||
});
|
||||
|
||||
test("reply: stop replying button click", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Reply')");
|
||||
await contains(".o-mail-Composer");
|
||||
await contains("i[title='Stop replying']");
|
||||
await click("i[title='Stop replying']");
|
||||
await contains(".o-mail-Composer", { count: 0 });
|
||||
});
|
||||
|
||||
test("error notifications should not be shown in Inbox", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message-header small", { text: "on Demo User" });
|
||||
await contains(`.o-mail-Message-header a[href*='/odoo/res.partner/${partnerId}']`, {
|
||||
text: "Demo User",
|
||||
});
|
||||
await contains(".o-mail-Message-notification", { count: 0 });
|
||||
});
|
||||
|
||||
test("emptying inbox displays rainbow man in inbox", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const messageId1 = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId1,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("button:enabled", { text: "Mark all read" });
|
||||
await contains(".o_reward_rainbow");
|
||||
});
|
||||
|
||||
test("emptying inbox doesn't display rainbow man in another thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { text: "1" }] });
|
||||
const [partner] = pyEnv["res.partner"].read(serverState.partnerId);
|
||||
pyEnv["bus.bus"]._sendone(partner, "mail.message/mark_as_read", {
|
||||
message_ids: [messageId],
|
||||
needaction_inbox_counter: 0,
|
||||
});
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { count: 0 }] });
|
||||
// weak test, no guarantee that we waited long enough for the potential rainbow man to show
|
||||
await contains(".o_reward_rainbow", { count: 0 });
|
||||
});
|
||||
|
||||
test("Counter should be incremented by 1 when receiving a message with a mention in a channel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Thread" });
|
||||
const partnerUserId = pyEnv["res.partner"].create({ name: "partner1" });
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerUserId });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { text: "1" }] });
|
||||
const mention = [serverState.partnerId];
|
||||
const mentionName = serverState.partnerName;
|
||||
withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: {
|
||||
body: `<a href="https://www.hoot.test/odoo/res.partner/17" class="o_mail_redirect" data-oe-id="${mention[0]}" data-oe-model="res.partner" target="_blank" contenteditable="false">@${mentionName}</a> mention`,
|
||||
message_type: "comment",
|
||||
partner_ids: mention,
|
||||
subtype_xmlid: "mail.mt_comment",
|
||||
},
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains("button", { text: "Inbox", contains: [".badge", { text: "2" }] });
|
||||
});
|
||||
|
||||
test("Clear need action counter when opening a channel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const [messageId1, messageId2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
},
|
||||
{
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
needaction: true,
|
||||
res_id: channelId,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId1,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId2,
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-DiscussSidebar-item", {
|
||||
text: "General",
|
||||
contains: [".badge", { text: "2" }],
|
||||
});
|
||||
await click(".o-mail-DiscussSidebarChannel", { text: "General" });
|
||||
await contains(".o-mail-DiscussSidebar-item", {
|
||||
text: "General",
|
||||
contains: [".badge", { count: 0 }],
|
||||
});
|
||||
});
|
||||
|
||||
test("can reply to email message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
author_id: null,
|
||||
email_from: "md@oilcompany.fr",
|
||||
body: "an email message",
|
||||
model: "res.partner",
|
||||
needaction: true,
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "sent",
|
||||
notification_type: "inbox",
|
||||
res_partner_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss("mail.box_inbox");
|
||||
await contains(".o-mail-Message");
|
||||
await click("[title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Reply')");
|
||||
await contains(".o-mail-Composer", { text: "Replying to md@oilcompany.fr" });
|
||||
});
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, Deferred, press, queryFirst, tick } from "@odoo/hoot-dom";
|
||||
import {
|
||||
asyncStep,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import {
|
||||
SIZES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
isInViewportOf,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { PRESENT_VIEWPORT_THRESHOLD } from "@mail/core/common/thread";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Basic jump to present when scrolling to outdated messages", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
for (let i = 0; i < 20; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Non Empty Body ".repeat(100),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 20 });
|
||||
await contains(".o-mail-Thread");
|
||||
expect(document.querySelector(".o-mail-Thread").scrollHeight).toBeGreaterThan(
|
||||
PRESENT_VIEWPORT_THRESHOLD * document.querySelector(".o-mail-Thread").clientHeight,
|
||||
{ message: "should have enough scroll height to trigger jump to present" }
|
||||
);
|
||||
await click("[title='Jump to Present']");
|
||||
await contains("[title='Jump to Present']", { count: 0 });
|
||||
await contains(".o-mail-Thread", { scroll: "bottom" });
|
||||
});
|
||||
|
||||
test("Basic jump to present when scrolling to outdated messages (DESC, chatter aside)", async () => {
|
||||
patchUiSize({ size: SIZES.XXL });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
for (let i = 0; i < 20; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Non Empty Body ".repeat(100),
|
||||
message_type: "comment",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message", { count: 20 });
|
||||
await contains(".o-mail-Thread");
|
||||
expect(document.querySelector(".o-mail-Chatter").scrollHeight).toBeGreaterThan(
|
||||
PRESENT_VIEWPORT_THRESHOLD * document.querySelector(".o-mail-Chatter").clientHeight,
|
||||
{ message: "should have enough scroll height to trigger jump to present" }
|
||||
);
|
||||
await contains(".o-mail-Chatter", { scroll: 0 });
|
||||
await scroll(".o-mail-Chatter", "bottom");
|
||||
await isInViewportOf("[title='Jump to Present']", ".o-mail-Thread");
|
||||
await click("[title='Jump to Present']");
|
||||
await contains("[title='Jump to Present']", { count: 0 });
|
||||
await contains(".o-mail-Chatter", { scroll: 0 });
|
||||
});
|
||||
|
||||
test("Basic jump to present when scrolling to outdated messages (DESC, chatter non-aside)", async () => {
|
||||
patchUiSize({ size: SIZES.MD });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
for (let i = 0; i < 20; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Non Empty Body ".repeat(100),
|
||||
message_type: "comment",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
}
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message", { count: 20 });
|
||||
await contains(".o_content");
|
||||
expect(document.querySelector(".o_content").scrollHeight).toBeGreaterThan(
|
||||
PRESENT_VIEWPORT_THRESHOLD * document.querySelector(".o_content").clientHeight,
|
||||
{ message: "should have enough scroll height to trigger jump to present" }
|
||||
);
|
||||
await contains(".o_content", { scroll: 0 });
|
||||
await scroll(".o_content", "bottom");
|
||||
await isInViewportOf("[title='Jump to Present']", ".o_content");
|
||||
await click("[title='Jump to Present']");
|
||||
await contains("[title='Jump to Present']", { count: 0 });
|
||||
await contains(".o_content", { scroll: 0 });
|
||||
});
|
||||
|
||||
test("Jump to old reply should prompt jump to present", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const oldestMessageId = pyEnv["mail.message"].create({
|
||||
body: "<p>Hello world!</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
for (let i = 0; i < 100; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<p>Non Empty Body</p>".repeat(100),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
/**
|
||||
* The first message following the oldest message should have it as its parent message
|
||||
* so that the oldest message is inserted through the parent field during "load around"
|
||||
* to have the coverage of this part of the code (in particular having parent message
|
||||
* body being inserted with markup).
|
||||
*/
|
||||
parent_id: i === 0 ? oldestMessageId : undefined,
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
const newestMessageId = pyEnv["mail.message"].create({
|
||||
body: "Most Recent!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
parent_id: oldestMessageId,
|
||||
});
|
||||
const [selfMember] = pyEnv["discuss.channel.member"].search_read([
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
["channel_id", "=", channelId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([selfMember.id], {
|
||||
new_message_separator: newestMessageId + 1,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await click(".o-mail-MessageInReply .cursor-pointer");
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await contains(":nth-child(1 of .o-mail-Message)", { text: "Hello world!" });
|
||||
await click("[title='Jump to Present']");
|
||||
await contains("[title='Jump to Present']", { count: 0 });
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await contains(".o-mail-Thread", { scroll: "bottom" });
|
||||
});
|
||||
|
||||
test("Jump to old reply should prompt jump to present (RPC small delay)", async () => {
|
||||
// same test as before but with a small RPC delay
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const oldestMessageId = pyEnv["mail.message"].create({
|
||||
body: "<p>Hello world!</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
for (let i = 0; i < 100; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<p>Non Empty Body</p>".repeat(100),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
/**
|
||||
* The first message following the oldest message should have it as its parent message
|
||||
* so that the oldest message is inserted through the parent field during "load around"
|
||||
* to have the coverage of this part of the code (in particular having parent message
|
||||
* body being inserted with markup).
|
||||
*/
|
||||
parent_id: i === 0 ? oldestMessageId : undefined,
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
const newestMessageId = pyEnv["mail.message"].create({
|
||||
body: "Most Recent!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
parent_id: oldestMessageId,
|
||||
});
|
||||
const [selfMember] = pyEnv["discuss.channel.member"].search_read([
|
||||
["partner_id", "=", serverState.partnerId],
|
||||
["channel_id", "=", channelId],
|
||||
]);
|
||||
pyEnv["discuss.channel.member"].write([selfMember.id], {
|
||||
new_message_separator: newestMessageId + 1,
|
||||
});
|
||||
onRpcBefore("/discuss/channel/messages", tick); // small delay
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await click(".o-mail-MessageInReply .cursor-pointer");
|
||||
await click("[title='Jump to Present']");
|
||||
await contains("[title='Jump to Present']", { count: 0 });
|
||||
await contains(".o-mail-Thread", { scroll: "bottom" });
|
||||
});
|
||||
|
||||
test("Post message when seeing old message should jump to present", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const oldestMessageId = pyEnv["mail.message"].create({
|
||||
body: "<p>Hello world!</p>",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
for (let i = 0; i < 100; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<p>Non Empty Body</p>".repeat(100),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
/**
|
||||
* The first message following the oldest message should have it as its parent message
|
||||
* so that the oldest message is inserted through the parent field during "load around"
|
||||
* to have the coverage of this part of the code (in particular having parent message
|
||||
* body being inserted with markup).
|
||||
*/
|
||||
parent_id: i === 0 ? oldestMessageId : undefined,
|
||||
res_id: channelId,
|
||||
});
|
||||
}
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Most Recent!",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
parent_id: oldestMessageId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await click(".o-mail-MessageInReply .cursor-pointer");
|
||||
await contains("[title='Jump to Present']");
|
||||
await insertText(".o-mail-Composer-input", "Newly posted");
|
||||
await press("Enter");
|
||||
await contains("[title='Jump to Present']", { count: 0 });
|
||||
await contains(".o-mail-Thread", { scroll: "bottom" });
|
||||
await contains(".o-mail-Message-content", {
|
||||
text: "Newly posted",
|
||||
after: [".o-mail-Message-content", { text: "Most Recent!" }], // should load around present
|
||||
});
|
||||
});
|
||||
|
||||
test("when triggering jump to present, keeps showing old messages until recent ones are loaded", async () => {
|
||||
// make scroll behavior instantaneous.
|
||||
patchWithCleanup(Element.prototype, {
|
||||
scrollIntoView() {
|
||||
return super.scrollIntoView(true);
|
||||
},
|
||||
scrollTo(...args) {
|
||||
if (typeof args[0] === "object" && args[0]?.behavior === "smooth") {
|
||||
return super.scrollTo({ ...args[0], behavior: "instant" });
|
||||
}
|
||||
return super.scrollTo(...args);
|
||||
},
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
for (let i = 0; i < 60; i++) {
|
||||
pyEnv["mail.message"].create({
|
||||
body: i === 0 ? "first-message" : "Non Empty Body ".repeat(100),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
pinned_at: i === 0 ? "2020-02-12 08:30:00" : undefined,
|
||||
});
|
||||
}
|
||||
let slowMessageFetchDeferred;
|
||||
onRpcBefore("/discuss/channel/messages", async () => {
|
||||
asyncStep("/discuss/channel/messages");
|
||||
await slowMessageFetchDeferred;
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await waitForSteps(["/discuss/channel/messages"]);
|
||||
await click("[title='Pinned Messages']");
|
||||
await click(".o-discuss-PinnedMessagesPanel a[role='button']", { text: "Jump" });
|
||||
await contains(".o-mail-Thread .o-mail-Message", { text: "first-message" });
|
||||
await animationFrame();
|
||||
slowMessageFetchDeferred = new Deferred();
|
||||
await click("[title='Jump to Present']");
|
||||
await animationFrame();
|
||||
await waitForSteps(["/discuss/channel/messages"]);
|
||||
await contains(".o-mail-Thread .o-mail-Message", { text: "first-message" });
|
||||
slowMessageFetchDeferred.resolve();
|
||||
await contains(".o-mail-Thread .o-mail-Message", { text: "first-message", count: 0 });
|
||||
await contains(".o-mail-Thread", { scroll: "bottom" });
|
||||
});
|
||||
|
||||
test("focus composer after jump to present", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create(
|
||||
[...Array(40).keys()].map((i) => ({
|
||||
body: `<p>Non Empty Message ${i}</p>`,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
}))
|
||||
);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 30 });
|
||||
await contains(".o-mail-Composer.o-focused");
|
||||
queryFirst(".o-mail-Composer-input").blur();
|
||||
await contains(".o-mail-Composer.o-focused", { count: 0 });
|
||||
await click("[title='Jump to Present']");
|
||||
await contains(".o-mail-Composer.o-focused");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
261
odoo-bringout-oca-ocb-mail/mail/static/tests/emoji/emoji.test.js
Normal file
261
odoo-bringout-oca-ocb-mail/mail/static/tests/emoji/emoji.test.js
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import {
|
||||
defineParams,
|
||||
patchWithCleanup,
|
||||
preloadBundle,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
triggerHotkey,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, getFixture, test } from "@odoo/hoot";
|
||||
|
||||
import { queryFirst } from "@odoo/hoot-dom";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
preloadBundle("web.assets_emoji");
|
||||
|
||||
test("emoji picker correctly handles translations with special characters", async () => {
|
||||
defineParams({
|
||||
translations: {
|
||||
"Japanese “here” button": `Bouton "ici" japonais`,
|
||||
"heavy dollar sign": `Symbole du dollar\nlourd`,
|
||||
},
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await insertText(".o-EmojiPicker-search input", "ici");
|
||||
await contains(`.o-Emoji[title='Bouton "ici" japonais']`);
|
||||
await insertText(".o-EmojiPicker-search input", "dollar", { replace: true });
|
||||
await contains(`.o-Emoji[title*='Symbole du dollar']`);
|
||||
});
|
||||
|
||||
test("search emoji from keywords", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await insertText("input[placeholder='Search emoji']", "mexican");
|
||||
await contains(".o-Emoji", { text: "🌮" });
|
||||
await insertText(".o-EmojiPicker-search input", "9", { replace: true });
|
||||
await contains(".o-Emoji:eq(0)", { text: "🕘" });
|
||||
await contains(".o-Emoji:eq(1)", { text: "🕤" });
|
||||
await contains(".o-Emoji:eq(2)", { text: "9️⃣" });
|
||||
});
|
||||
|
||||
test("search emoji from keywords should be case insensitive", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await insertText("input[placeholder='Search emoji']", "ok");
|
||||
await contains(".o-Emoji", { text: "🆗" }); // all search terms are uppercase OK
|
||||
});
|
||||
|
||||
test("search emoji from keywords with special regex character", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await insertText("input[placeholder='Search emoji']", "(blood");
|
||||
await contains(".o-Emoji", { text: "🆎" });
|
||||
});
|
||||
|
||||
test("updating search emoji should scroll top", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-content", { scroll: 0 });
|
||||
await scroll(".o-EmojiPicker-content", 150);
|
||||
await insertText("input[placeholder='Search emoji']", "m");
|
||||
await contains(".o-EmojiPicker-content", { scroll: 0 });
|
||||
});
|
||||
|
||||
test("Press Escape in emoji picker closes the emoji picker", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
triggerHotkey("Escape");
|
||||
await contains(".o-EmojiPicker", { count: 0 });
|
||||
});
|
||||
|
||||
test("Basic keyboard navigation", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Composer-input:focus"); // as to ensure no race condition with auto-focus of emoji picker
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-Emoji[data-index='0'].o-active");
|
||||
// detect amount of emojis per row for navigation
|
||||
const emojis = Array.from(
|
||||
getFixture().querySelectorAll(".o-EmojiPicker-category[data-category='1'] ~ .o-Emoji")
|
||||
);
|
||||
const baseOffset = emojis[0].offsetTop;
|
||||
const breakIndex = emojis.findIndex((item) => item.offsetTop > baseOffset);
|
||||
const EMOJI_PER_ROW = breakIndex === -1 ? emojis.length : breakIndex;
|
||||
triggerHotkey("ArrowRight");
|
||||
await contains(".o-EmojiPicker-content .o-Emoji[data-index='1'].o-active");
|
||||
triggerHotkey("ArrowDown");
|
||||
await contains(`.o-EmojiPicker-content .o-Emoji[data-index='${EMOJI_PER_ROW + 1}'].o-active`);
|
||||
triggerHotkey("ArrowLeft");
|
||||
await contains(`.o-EmojiPicker-content .o-Emoji[data-index='${EMOJI_PER_ROW}'].o-active`);
|
||||
triggerHotkey("ArrowUp");
|
||||
await contains(".o-EmojiPicker-content .o-Emoji[data-index='0'].o-active");
|
||||
const { codepoints } = queryFirst(
|
||||
".o-EmojiPicker-content .o-Emoji[data-index='0'].o-active"
|
||||
).dataset;
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-EmojiPicker", { count: 0 });
|
||||
await contains(".o-mail-Composer-input", { value: codepoints });
|
||||
});
|
||||
|
||||
test("recent category (basic)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-navbar [title='Frequently used']", { count: 0 });
|
||||
await click(".o-EmojiPicker-content .o-Emoji", { text: "😀" });
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-navbar [title='Frequently used']");
|
||||
await contains(".o-Emoji", {
|
||||
text: "😀",
|
||||
after: ["span", { textContent: "Frequently used" }],
|
||||
before: ["span", { textContent: "Smileys & Emotion" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("search emojis prioritize frequently used emojis", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-navbar [title='Frequently used']", { count: 0 });
|
||||
await click(".o-EmojiPicker-content .o-Emoji", { text: "🤥" });
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-navbar [title='Frequently used']");
|
||||
await insertText("input[placeholder='Search emoji']", "lie");
|
||||
await contains(".o-EmojiPicker-sectionIcon", { count: 0 }); // await search performed
|
||||
await contains(".o-EmojiPicker-content .o-Emoji:eq(0)", { text: "🤥" });
|
||||
});
|
||||
|
||||
test("search matches only frequently used emojis", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-navbar [title='Frequently used']", { count: 0 });
|
||||
await click(".o-EmojiPicker-content .o-Emoji", { text: "🥦" });
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-navbar [title='Frequently used']");
|
||||
await insertText(".o-EmojiPicker-search input", "brocoli");
|
||||
await contains(".o-EmojiPicker-sectionIcon", { count: 0 }); // await search performed
|
||||
await contains(".o-EmojiPicker-content .o-Emoji:eq(0)", { text: "🥦" });
|
||||
await contains(".o-EmojiPicker-content .o-Emoji", { count: 1 });
|
||||
await contains(".o-EmojiPicker-content:has(:text('No emojis match your search'))", {
|
||||
count: 0,
|
||||
});
|
||||
await insertText(".o-EmojiPicker-search input", "2");
|
||||
await contains(".o-EmojiPicker-content:has(:text('No emojis match your search'))");
|
||||
});
|
||||
|
||||
test("emoji usage amount orders frequent emojis", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await click(".o-EmojiPicker-content .o-Emoji", { text: "😀" });
|
||||
await click("button[title='Add Emojis']");
|
||||
await click(".o-EmojiPicker-content .o-Emoji", { text: "👽" });
|
||||
await click("button[title='Add Emojis']");
|
||||
await click(".o-EmojiPicker-content .o-Emoji", { text: "👽" });
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-Emoji", {
|
||||
text: "👽",
|
||||
after: ["span", { textContent: "Frequently used" }],
|
||||
before: [
|
||||
".o-Emoji",
|
||||
{
|
||||
text: "😀",
|
||||
after: ["span", { textContent: "Frequently used" }],
|
||||
before: ["span", { textContent: "Smileys & Emotion" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("first category should be highlighted by default", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker-navbar :nth-child(1 of .o-Emoji).o-active");
|
||||
});
|
||||
|
||||
test("selecting an emoji while holding down the Shift key prevents the emoji picker from closing", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await click(".o-EmojiPicker-content .o-Emoji", { shiftKey: true, text: "👺" });
|
||||
await contains(".o-EmojiPicker-navbar [title='Frequently used']");
|
||||
await contains(".o-EmojiPicker");
|
||||
await contains(".o-mail-Composer-input", { value: "👺" });
|
||||
});
|
||||
|
||||
test("shortcodes shown in emoji title in message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
pyEnv["mail.message"].create({
|
||||
res_id: channelId,
|
||||
model: "discuss.channel",
|
||||
body: "💑😇",
|
||||
author_id: serverState.partnerId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { text: "💑😇" });
|
||||
await contains(".o-mail-Message span[title=':couple_with_heart:']", { text: "💑" });
|
||||
await contains(".o-mail-Message span[title=':innocent: :halo:']", { text: "😇" });
|
||||
});
|
||||
|
||||
test("Emoji picker shows failure to load emojis", async () => {
|
||||
// Simulate failure to load emojis
|
||||
patchWithCleanup(odoo.loader.modules.get("@web/core/emoji_picker/emoji_data"), {
|
||||
getEmojis() {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add Emojis']");
|
||||
await contains(".o-EmojiPicker", { text: "😵💫Failed to load emojis..." });
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
|
||||
import { formatText } from "@mail/js/emojis_mixin";
|
||||
|
||||
test("Emoji formatter handles compound emojis", () => {
|
||||
const testString = "<p>👩🏿test👩🏿👩t👩</p>";
|
||||
const expectedString =
|
||||
"<p><span class='o_mail_emoji'>👩🏿</span>test<span class='o_mail_emoji'>👩🏿👩</span>t<span class='o_mail_emoji'>👩</span></p>";
|
||||
expect(formatText(testString).toString()).toBe(expectedString);
|
||||
});
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
import {
|
||||
SIZES,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
focus,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
openFormView,
|
||||
patchUiSize,
|
||||
scroll,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
asyncStep,
|
||||
getService,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
preloadBundle,
|
||||
waitForSteps,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { GifPicker } from "@mail/discuss/gif_picker/common/gif_picker";
|
||||
import { animationFrame, queryFirst } from "@odoo/hoot-dom";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
preloadBundle("web.assets_emoji");
|
||||
|
||||
let gifId = 0;
|
||||
const gifFactory = (count = 1, options = {}) => {
|
||||
const gifs = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
gifs.push({
|
||||
id: `${gifId}`,
|
||||
title: "",
|
||||
media_formats: {
|
||||
tinygif: {
|
||||
url: options.url || "https://media.tenor.com/np49Y1vrJO8AAAAM/crying-cry.gif",
|
||||
duration: 0,
|
||||
preview: "",
|
||||
dims: [220, 190],
|
||||
size: 1007885,
|
||||
},
|
||||
},
|
||||
created: 1654414453.782169,
|
||||
content_description: "Cry GIF",
|
||||
itemurl: "https://tenor.com/view/cry-gif-25866484",
|
||||
url: "https://tenor.com/bUHdw.gif",
|
||||
tags: ["cry"],
|
||||
flags: [],
|
||||
hasaudio: false,
|
||||
});
|
||||
gifId++;
|
||||
}
|
||||
return gifs;
|
||||
};
|
||||
|
||||
const rpc = {
|
||||
search: {
|
||||
results: gifFactory(2),
|
||||
next: "CAgQpIGj_8WN_gIaHgoKAD-_xMQ20dMU_xIQ1MVHUnSAQxC98Y6VAAAAADAI",
|
||||
},
|
||||
categories: {
|
||||
locale: "en",
|
||||
tags: [
|
||||
{
|
||||
searchterm: "cry",
|
||||
path: "/v2/search?q=cry&locale=en&component=categories&contentfilter=low",
|
||||
image: "https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif",
|
||||
name: "#cry",
|
||||
},
|
||||
{
|
||||
searchterm: "yes",
|
||||
path: "/v2/search?q=yes&locale=en&component=categories&contentfilter=low",
|
||||
image: "https://media.tenor.com/UVmpVqlpVhQAAAAM/yess-yes.gif",
|
||||
name: "#yes",
|
||||
},
|
||||
{
|
||||
searchterm: "no",
|
||||
path: "/v2/search?q=no&locale=en&component=categories&contentfilter=low",
|
||||
image: "https://media.tenor.com/aeswYw-86k8AAAAM/no-nooo.gif",
|
||||
name: "#no",
|
||||
},
|
||||
{
|
||||
searchterm: "lol",
|
||||
path: "/v2/search?q=lol&locale=en&component=categories&contentfilter=low",
|
||||
image: "https://media.tenor.com/BiseY2UXovAAAAAM/lmfao-laughing.gif",
|
||||
name: "#lol",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
test("composer should display a GIF button", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains("button[title='Add GIFs']");
|
||||
});
|
||||
|
||||
test("Composer GIF button should open the GIF picker (discuss app)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await contains(".o-discuss-GifPicker");
|
||||
});
|
||||
|
||||
test("Composer GIF button should open the GIF picker (chat window)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem:contains('General')");
|
||||
await click(".o-mail-ChatWindow .o-mail-Composer [title='More Actions']");
|
||||
await click(".o-dropdown-item:contains('Add GIFs')");
|
||||
await contains(".o-discuss-GifPicker");
|
||||
});
|
||||
|
||||
test("Not loading of GIF categories when feature is not available", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
let isFeatureEnabled = true;
|
||||
onRpc("/discuss/gif/categories", () => {
|
||||
asyncStep("/discuss/gif/categories");
|
||||
if (isFeatureEnabled) {
|
||||
return rpc.categories;
|
||||
}
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
const store = getService("mail.store");
|
||||
store.hasGifPickerFeature = false;
|
||||
isFeatureEnabled = false;
|
||||
await click("button[title='Add GIFs']");
|
||||
await contains(".o-discuss-GifPicker");
|
||||
await animationFrame();
|
||||
expect.verifySteps([]); // no "/discuss/gif/categories"
|
||||
await click("button[title='Add GIFs']");
|
||||
await contains(".o-discuss-GifPicker", { count: 0 });
|
||||
store.hasGifPickerFeature = true;
|
||||
isFeatureEnabled = true;
|
||||
await click("button[title='Add GIFs']");
|
||||
await contains(".o-discuss-GifPicker");
|
||||
await waitForSteps(["/discuss/gif/categories"]);
|
||||
});
|
||||
|
||||
test("Searching for a GIF", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/search", () => rpc.search);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await insertText("input[placeholder='Search for a GIF']", "search");
|
||||
await contains("i[aria-label='back']");
|
||||
await contains(".o-discuss-Gif", { count: 2 });
|
||||
});
|
||||
|
||||
test("Open a GIF category trigger the search for the category", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
onRpc("/discuss/gif/search", () => rpc.search);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await click("img[data-src='https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif']");
|
||||
await contains(".o-discuss-Gif", { count: 2 });
|
||||
await contains("input[placeholder='Search for a GIF']", { value: "cry" });
|
||||
});
|
||||
|
||||
test("Can have GIF categories with same name", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => ({
|
||||
locale: "en",
|
||||
tags: [
|
||||
{
|
||||
searchterm: "duplicate",
|
||||
path: "/v2/search?q=duplicate&locale=en&component=categories&contentfilter=low",
|
||||
image: "https://media.tenor.com/BiseY2UXovAAAAAM/duplicate.gif",
|
||||
name: "#duplicate",
|
||||
},
|
||||
{
|
||||
searchterm: "duplicate",
|
||||
path: "/v2/search?q=duplicate&locale=en&component=categories&contentfilter=low",
|
||||
image: "https://media.tenor.com/BiseY2UXovAAAAAM/duplicate.gif",
|
||||
name: "#duplicate",
|
||||
},
|
||||
],
|
||||
}));
|
||||
onRpc("/discuss/gif/search", () => rpc.search);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await contains("img[data-src='https://media.tenor.com/BiseY2UXovAAAAAM/duplicate.gif']", {
|
||||
count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("Reopen GIF category list when going back", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
onRpc("/discuss/gif/search", () => rpc.search);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await click("img[data-src='https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif']");
|
||||
await click("i[aria-label='back']");
|
||||
await contains(".o-discuss-GifPicker div[aria-label='list']");
|
||||
});
|
||||
|
||||
test("Add GIF to favorite", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
onRpc("/discuss/gif/search", () => rpc.search);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await click("img[data-src='https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif']");
|
||||
await click(":nth-child(1 of div) > .o-discuss-Gif .fa-star-o");
|
||||
await contains(".o-discuss-Gif .fa-star");
|
||||
await click("i[aria-label='back']");
|
||||
await click(".o-discuss-GifPicker div[aria-label='list-item']", { text: "Favorites" });
|
||||
await contains(".o-discuss-Gif");
|
||||
});
|
||||
|
||||
test("Chatter should not have the GIF button", async () => {
|
||||
const pyEnv = await startServer();
|
||||
await start();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await click("button", { text: "Log note" });
|
||||
await contains("button[title='Add GIFs']", { count: 0 });
|
||||
});
|
||||
|
||||
test("Composer GIF button should open the GIF picker keyboard in footer", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='More Actions']");
|
||||
await click(".dropdown-item:contains('Add GIFs')");
|
||||
await contains(".o-mail-Composer-footer .o-discuss-GifPicker");
|
||||
});
|
||||
|
||||
test("Searching for a GIF with a failling RPC should display an error", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
onRpc("/discuss/gif/search", () => {
|
||||
throw new Error("Rpc failed");
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await insertText("input[placeholder='Search for a GIF']", "search");
|
||||
await contains(".o-discuss-GifPicker-error");
|
||||
});
|
||||
|
||||
test("Scrolling at the bottom should trigger the search to load more gif, even after visiting the favorite.", async () => {
|
||||
patchWithCleanup(GifPicker.prototype, {
|
||||
get style() {
|
||||
return "width: 200px;height: 200px;background: #000";
|
||||
},
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
onRpc("/discuss/gif/search", () => {
|
||||
const _rpc = rpc.search;
|
||||
_rpc.results = gifFactory(4);
|
||||
return _rpc;
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
// gif picker quires extra delay before click (to give time to load initial state)
|
||||
await contains(".o-discuss-GifPicker");
|
||||
await click(".o-discuss-GifPicker div[aria-label='list-item']", { text: "Favorites" });
|
||||
await click("i[aria-label='back']");
|
||||
await click("img[data-src='https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif']");
|
||||
await contains(".o-discuss-Gif", { count: 4 });
|
||||
await scroll(".o-discuss-GifPicker-content", "bottom");
|
||||
await contains(".o-discuss-Gif", { count: 8 });
|
||||
});
|
||||
|
||||
test("Pause GIF when thread is not focused", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
onRpc("/discuss/gif/search", () => rpc.search);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await click("img[data-src='https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif']");
|
||||
await click("img[data-src='https://media.tenor.com/np49Y1vrJO8AAAAM/crying-cry.gif']:eq(0)");
|
||||
await contains(".o-mail-LinkPreviewImage");
|
||||
queryFirst(".o-mail-Thread").blur();
|
||||
await contains(".o-mail-LinkPreviewImage img[data-paused]");
|
||||
await focus(".o-mail-Thread");
|
||||
await contains(".o-mail-LinkPreviewImage img:not([data-paused])");
|
||||
});
|
||||
|
||||
test("Show help when no favorite GIF", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
// gif picker quires extra delay before click (to give time to load initial state)
|
||||
await contains(".o-discuss-GifPicker");
|
||||
await click(".o-discuss-GifPicker div[aria-label='list-item']", { text: "Favorites" });
|
||||
await contains("span", { text: "So uhh... maybe go favorite some GIFs?" });
|
||||
});
|
||||
|
||||
test("Clicking GIF preview does not raise an error", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "" });
|
||||
onRpc("/discuss/gif/categories", () => rpc.categories);
|
||||
onRpc("/discuss/gif/search", () => rpc.search);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click("button[title='Add GIFs']");
|
||||
await click("img[data-src='https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif']");
|
||||
await click("img[data-src='https://media.tenor.com/np49Y1vrJO8AAAAM/crying-cry.gif']:eq(0)");
|
||||
await click(".o-mail-LinkPreviewImage img");
|
||||
await contains(".o-mail-Message");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,111 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/controllers/discuss', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/mail/channel/notify_typing') {
|
||||
const id = args.channel_id;
|
||||
const is_typing = args.is_typing;
|
||||
const context = args.context;
|
||||
return this._mockRouteMailChannelNotifyTyping(id, is_typing, context);
|
||||
}
|
||||
if (route === '/mail/channel/ping') {
|
||||
return;
|
||||
}
|
||||
if (route === '/mail/rtc/channel/join_call') {
|
||||
return this._mockRouteMailRtcChannelJoinCall(args.channel_id, args.check_rtc_session_ids);
|
||||
}
|
||||
if (route === '/mail/rtc/channel/leave_call') {
|
||||
return this._mockRouteMailRtcChannelLeaveCall(args.channel_id);
|
||||
}
|
||||
if (route === '/mail/rtc/session/update_and_broadcast') {
|
||||
return;
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/channel/notify_typing` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channel_id
|
||||
* @param {integer} limit
|
||||
* @param {Object} [context={}]
|
||||
*/
|
||||
async _mockRouteMailChannelNotifyTyping(channel_id, is_typing, context = {}) {
|
||||
const partnerId = context.mockedPartnerId || this.currentPartnerId;
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerId]]);
|
||||
if (!memberOfCurrentUser) {
|
||||
return;
|
||||
}
|
||||
this._mockMailChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/rtc/channel/join_call` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channel_id
|
||||
* @returns {integer[]} [check_rtc_session_ids]
|
||||
*/
|
||||
async _mockRouteMailRtcChannelJoinCall(channel_id, check_rtc_session_ids = []) {
|
||||
const [currentChannelMember] = this.getRecords('mail.channel.member', [
|
||||
['channel_id', '=', channel_id],
|
||||
['partner_id', '=', this.currentPartnerId],
|
||||
]);
|
||||
const sessionId = this.pyEnv['mail.channel.rtc.session'].create({
|
||||
channel_member_id: currentChannelMember.id,
|
||||
channel_id, // on the server, this is a related field from channel_member_id and not explicitly set
|
||||
});
|
||||
const channelMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id]]);
|
||||
const rtcSessions = this.getRecords('mail.channel.rtc.session', [
|
||||
['channel_member_id', 'in', channelMembers.map(channelMember => channelMember.id)],
|
||||
]);
|
||||
return {
|
||||
'iceServers': false,
|
||||
'rtcSessions': [
|
||||
['insert', rtcSessions.map(rtcSession => this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id))],
|
||||
],
|
||||
'sessionId': sessionId,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/rtc/channel/leave_call` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channelId
|
||||
*/
|
||||
async _mockRouteMailRtcChannelLeaveCall(channel_id) {
|
||||
const channelMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id]]);
|
||||
const rtcSessions = this.getRecords('mail.channel.rtc.session', [
|
||||
['channel_member_id', 'in', channelMembers.map(channelMember => channelMember.id)],
|
||||
]);
|
||||
const notifications = [];
|
||||
const channelInfo = this._mockMailChannelRtcSession_MailChannelRtcSessionFormatByChannel(rtcSessions.map(rtcSession => rtcSession.id));
|
||||
for (const [channelId, sessionsData] of Object.entries(channelInfo)) {
|
||||
const notificationRtcSessions = sessionsData.map((sessionsDataPoint) => {
|
||||
return { 'id': sessionsDataPoint.id };
|
||||
});
|
||||
notifications.push([
|
||||
channelId,
|
||||
'mail.channel/rtc_sessions_update',
|
||||
{
|
||||
'id': Number(channelId), // JS object keys are strings, but the type from the server is number
|
||||
'rtcSessions': [['insert-and-unlink', notificationRtcSessions]],
|
||||
}
|
||||
]);
|
||||
}
|
||||
for (const rtcSession of rtcSessions) {
|
||||
const target = rtcSession.guest_id || rtcSession.partner_id;
|
||||
notifications.push([
|
||||
target,
|
||||
'mail.channel.rtc.session/ended',
|
||||
{ 'sessionId': rtcSession.id },
|
||||
]);
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendmany(notifications);
|
||||
},
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
// ensure bus override is applied first.
|
||||
import "@bus/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/ir_websocket', {
|
||||
/**
|
||||
* Simulates `_get_im_status` on `ir.websocket`.
|
||||
*
|
||||
* @param {Object} imStatusIdsByModel
|
||||
* @param {Number[]|undefined} mail.guest ids of mail.guest whose im_status
|
||||
* should be monitored.
|
||||
*/
|
||||
_mockIrWebsocket__getImStatus(imStatusIdsByModel) {
|
||||
const imStatus = this._super(imStatusIdsByModel);
|
||||
const { 'mail.guest': guestIds } = imStatusIdsByModel;
|
||||
if (guestIds) {
|
||||
imStatus['guests'] = this.pyEnv['mail.guest'].searchRead([['id', 'in', guestIds]], { context: { 'active_test': false }, fields: ['im_status'] });
|
||||
}
|
||||
return imStatus;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_channel_member', {
|
||||
/**
|
||||
* Simulates `notify_typing` on `mail.channel.member`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @param {boolean} is_typing
|
||||
*/
|
||||
_mockMailChannelMember_NotifyTyping(ids, is_typing) {
|
||||
const members = this.getRecords('mail.channel.member', [['id', 'in', ids]]);
|
||||
const notifications = [];
|
||||
for (const member of members) {
|
||||
const [channel] = this.getRecords('mail.channel', [['id', '=', member.channel_id]]);
|
||||
const [data] = this._mockMailChannelMember_MailChannelMemberFormat([member.id]);
|
||||
Object.assign(data, {
|
||||
'isTyping': is_typing,
|
||||
});
|
||||
notifications.push([channel, 'mail.channel.member/typing_status', data]);
|
||||
notifications.push([channel.uuid, 'mail.channel.member/typing_status', data]);
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendmany(notifications);
|
||||
},
|
||||
/**
|
||||
* Simulates `_mail_channel_member_format` on `mail.channel.member`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailChannelMember_MailChannelMemberFormat(ids) {
|
||||
const members = this.getRecords('mail.channel.member', [['id', 'in', ids]]);
|
||||
const dataList = [];
|
||||
for (const member of members) {
|
||||
let persona;
|
||||
if (member.partner_id) {
|
||||
persona = { 'partner': this._mockMailChannelMember_GetPartnerData([member.id]) };
|
||||
}
|
||||
if (member.guest_id) {
|
||||
const [guest] = this.getRecords('mail.guest', [['id', '=', member.guest_id]]);
|
||||
persona = {
|
||||
'guest': {
|
||||
'id': guest.id,
|
||||
'im_status': guest.im_status,
|
||||
'name': guest.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
const data = {
|
||||
'channel': { 'id': member.channel_id },
|
||||
'id': member.id,
|
||||
'persona': persona,
|
||||
};
|
||||
dataList.push(data);
|
||||
}
|
||||
return dataList;
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_partner_data` on `mail.channel.member`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailChannelMember_GetPartnerData(ids) {
|
||||
const [member] = this.getRecords('mail.channel.member', [['id', 'in', ids]]);
|
||||
return this._mockResPartnerMailPartnerFormat([member.partner_id]).get(member.partner_id);
|
||||
},
|
||||
});
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_channel_rtc_session', {
|
||||
/**
|
||||
* Simulates `_mail_rtc_session_format` on `mail.channel.rtc.session`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailChannelRtcSession_MailChannelRtcSessionFormat(id) {
|
||||
const [rtcSession] = this.getRecords('mail.channel.rtc.session', [['id', '=', id]]);
|
||||
return {
|
||||
'id': rtcSession.id,
|
||||
'channelMember': this._mockMailChannelMember_MailChannelMemberFormat([rtcSession.channel_member_id])[0],
|
||||
'isCameraOn': rtcSession.is_camera_on,
|
||||
'isDeaf': rtcSession.is_deaf,
|
||||
'isSelfMuted': rtcSession.is_self_muted,
|
||||
'isScreenSharingOn': rtcSession.is_screen_sharing_on,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates `_mail_rtc_session_format` on `mail.channel.rtc.session`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailChannelRtcSession_MailChannelRtcSessionFormatByChannel(ids) {
|
||||
const rtcSessions = this.getRecords('mail.channel.rtc.session', [['id', 'in', ids]]);
|
||||
const data = {};
|
||||
for (const rtcSession of rtcSessions) {
|
||||
if (!data[rtcSession.channel_id]) {
|
||||
data[rtcSession.channel_id] = [];
|
||||
}
|
||||
data[rtcSession.channel_id].push(this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { TEST_GROUP_IDS, TEST_USER_IDS } from '@bus/../tests/helpers/test_constants';
|
||||
import {
|
||||
addFakeModel,
|
||||
addModelNamesToFetch,
|
||||
insertModelFields,
|
||||
insertRecords
|
||||
} from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Models
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
addModelNamesToFetch([
|
||||
'mail.activity', 'mail.activity.type', 'mail.channel', 'mail.channel.member',
|
||||
'mail.channel.rtc.session', 'mail.followers', 'mail.guest', 'mail.link.preview', 'mail.message',
|
||||
'mail.message.subtype', 'mail.notification', 'mail.shortcode', 'mail.template',
|
||||
'mail.tracking.value', 'res.users.settings', 'res.users.settings.volumes'
|
||||
]);
|
||||
|
||||
addFakeModel('res.fake', {
|
||||
message_ids: { string: 'Messages', type: 'one2many', relation: 'mail.message' },
|
||||
activity_ids: { string: "Activities", type: 'one2many', relation: 'mail.activity' },
|
||||
email_cc: { type: 'char' },
|
||||
partner_ids: { relation: 'res.partner', string: "Related partners", type: 'one2many' },
|
||||
});
|
||||
|
||||
addFakeModel('m2x.avatar.user', {
|
||||
user_id: { type: 'many2one', relation: 'res.users' },
|
||||
user_ids: { type: 'many2many', relation: 'res.users' },
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Insertion of fields
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
insertModelFields('mail.activity', {
|
||||
chaining_type: { default: 'suggest' },
|
||||
});
|
||||
insertModelFields('mail.channel', {
|
||||
author_id: {
|
||||
default() {
|
||||
return this.currentPartnerId;
|
||||
},
|
||||
},
|
||||
avatarCacheKey: { string: "Avatar Cache Key", type: "datetime" },
|
||||
channel_member_ids: {
|
||||
default() {
|
||||
return [[0, 0, { partner_id: this.currentPartnerId }]];
|
||||
},
|
||||
},
|
||||
channel_type: { default: 'channel' },
|
||||
group_based_subscription: { string: "Group based subscription", type: "boolean" },
|
||||
group_public_id: {
|
||||
default() {
|
||||
return TEST_GROUP_IDS.groupUserId;
|
||||
},
|
||||
},
|
||||
uuid: { default: () => _.uniqueId('mail.channel_uuid-') },
|
||||
});
|
||||
insertModelFields('mail.channel.member', {
|
||||
fold_state: { default: 'open' },
|
||||
is_pinned: { default: true },
|
||||
message_unread_counter: { default: 0 },
|
||||
});
|
||||
insertModelFields('mail.message', {
|
||||
author_id: { default: TEST_USER_IDS.currentPartnerId },
|
||||
history_partner_ids: { relation: 'res.partner', string: "Partners with History", type: 'many2many' },
|
||||
is_discussion: { string: 'Discussion', type: 'boolean' },
|
||||
is_note: { string: "Discussion", type: 'boolean' },
|
||||
is_notification: { string: "Note", type: 'boolean' },
|
||||
needaction_partner_ids: { relation: 'res.partner', string: "Partners with Need Action", type: 'many2many' },
|
||||
res_model_name: { string: "Res Model Name", type: 'char' },
|
||||
});
|
||||
insertModelFields('mail.message.subtype', {
|
||||
subtype_xmlid: { type: 'char' },
|
||||
});
|
||||
insertModelFields('mail.tracking.value', {
|
||||
changed_field: { string: 'Changed field', type: 'char' },
|
||||
new_value: { string: 'New value', type: 'char' },
|
||||
old_value: { string: 'Old value', type: 'char' },
|
||||
});
|
||||
insertModelFields('res.users.settings', {
|
||||
is_discuss_sidebar_category_channel_open: { default: true },
|
||||
is_discuss_sidebar_category_chat_open: { default: true },
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Insertion of records
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
insertRecords('mail.activity.type', [
|
||||
{ icon: 'fa-envelope', id: 1, name: "Email" },
|
||||
{ icon: 'fa-upload', id: 28, name: "Upload Document" },
|
||||
]);
|
||||
insertRecords('mail.message.subtype', [
|
||||
{ default: false, internal: true, name: "Activities", sequence: 90, subtype_xmlid: 'mail.mt_activities' },
|
||||
{
|
||||
default: false, internal: true, name: "Note", sequence: 100, subtype_xmlid: 'mail.mt_note',
|
||||
track_recipients: true
|
||||
},
|
||||
{ name: "Discussions", sequence: 0, subtype_xmlid: 'mail.mt_comment', track_recipients: true },
|
||||
]);
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from '@web/core/browser/browser';
|
||||
import { MEDIAS_BREAKPOINTS, SIZES, uiService } from '@web/core/ui/ui_service';
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
|
||||
import config from 'web.config';
|
||||
|
||||
/**
|
||||
* Return the width corresponding to the given size. If an upper and lower bound
|
||||
* are defined, returns the lower bound: this is an arbitrary choice that should
|
||||
* not impact anything. A test should pass the `width` parameter instead of `size`
|
||||
* if it needs a specific width to be set.
|
||||
*
|
||||
* @param {number} size
|
||||
* @returns {number} The width corresponding to the given size.
|
||||
*/
|
||||
function getWidthFromSize(size) {
|
||||
const { minWidth, maxWidth } = MEDIAS_BREAKPOINTS[size];
|
||||
return minWidth ? minWidth : maxWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the size corresponding to the given width.
|
||||
*
|
||||
* @param {number} width
|
||||
* @returns {number} The size corresponding to the given width.
|
||||
*/
|
||||
function getSizeFromWidth(width) {
|
||||
return MEDIAS_BREAKPOINTS.findIndex(({ minWidth, maxWidth }) => {
|
||||
if (!maxWidth) {
|
||||
return width >= minWidth;
|
||||
}
|
||||
if (!minWidth) {
|
||||
return width <= maxWidth;
|
||||
}
|
||||
return width >= minWidth && width <= maxWidth;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch legacy objects referring to the ui size. This function must be removed
|
||||
* when the wowl env will be available in the form_renderer (currently the form
|
||||
* renderer relies on config). This will impact env.browser.innerWidth,
|
||||
* env.device.isMobile and config.device.{size_class/isMobile}.
|
||||
*
|
||||
* @param {number} size
|
||||
* @param {number} width
|
||||
*/
|
||||
function legacyPatchUiSize(height, size, width) {
|
||||
const legacyEnv = owl.Component.env;
|
||||
patchWithCleanup(legacyEnv, {
|
||||
browser: {
|
||||
...legacyEnv.browser,
|
||||
innerWidth: width,
|
||||
innerHeight: height || browser.innerHeight,
|
||||
},
|
||||
device: {
|
||||
...legacyEnv.device,
|
||||
isMobile: size <= SIZES.SM,
|
||||
}
|
||||
});
|
||||
patchWithCleanup(config, {
|
||||
device: {
|
||||
...config.device,
|
||||
size_class: size,
|
||||
isMobile: size <= SIZES.SM,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust ui size either from given size (mapped to window breakpoints) or
|
||||
* width. This will impact not only config.device.{size_class/isMobile} but
|
||||
* uiService.{isSmall/size}, (wowl/legacy) browser.innerWidth, (wowl)
|
||||
* env.isSmall and (legacy) env.device.isMobile. When a size is given, the browser
|
||||
* width is set according to the breakpoints that are used by the webClient.
|
||||
*
|
||||
* @param {Object} params parameters to configure the ui size.
|
||||
* @param {number|undefined} [params.size]
|
||||
* @param {number|undefined} [params.width]
|
||||
* @param {number|undefined} [params.height]
|
||||
*/
|
||||
function patchUiSize({ height, size, width }) {
|
||||
if (!size && !width || size && width) {
|
||||
throw new Error('Either size or width must be given to the patchUiSize function');
|
||||
}
|
||||
size = size === undefined ? getSizeFromWidth(width) : size;
|
||||
width = width || getWidthFromSize(size);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
innerWidth: width,
|
||||
innerHeight: height || browser.innerHeight,
|
||||
});
|
||||
patchWithCleanup(uiService, {
|
||||
getSize() {
|
||||
return size;
|
||||
},
|
||||
});
|
||||
legacyPatchUiSize(height, size, width);
|
||||
}
|
||||
|
||||
export {
|
||||
patchUiSize,
|
||||
SIZES
|
||||
};
|
||||
|
|
@ -1,398 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getPyEnv, startServer } from '@bus/../tests/helpers/mock_python_environment';
|
||||
|
||||
import { nextTick } from '@mail/utils/utils';
|
||||
import { getAdvanceTime } from '@mail/../tests/helpers/time_control';
|
||||
import { getWebClientReady } from '@mail/../tests/helpers/webclient_setup';
|
||||
|
||||
import { wowlServicesSymbol } from "@web/legacy/utils";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { getFixture, makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers";
|
||||
|
||||
const { App, EventBus } = owl;
|
||||
const { afterNextRender } = App;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a fake object 'dataTransfer', linked to some files,
|
||||
* which is passed to drag and drop events.
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _createFakeDataTransfer(files) {
|
||||
return {
|
||||
dropEffect: 'all',
|
||||
effectAllowed: 'all',
|
||||
files,
|
||||
items: [],
|
||||
types: ['Files'],
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public: rendering timers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a promise resolved at the next animation frame.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function nextAnimationFrame() {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public: test lifecycle
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function getAfterEvent({ messagingBus }) {
|
||||
/**
|
||||
* Returns a promise resolved after the expected event is received.
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.eventName event to wait
|
||||
* @param {function} param0.func function which, when called, is expected to
|
||||
* trigger the event
|
||||
* @param {string} [param0.message] assertion message
|
||||
* @param {function} [param0.predicate] predicate called with event data.
|
||||
* If not provided, only the event name has to match.
|
||||
* @param {number} [param0.timeoutDelay=5000] how long to wait at most in ms
|
||||
* @returns {Promise}
|
||||
*/
|
||||
return async function afterEvent({ eventName, func, message, predicate, timeoutDelay = 5000 }) {
|
||||
const error = new Error(message || `Timeout: the event ${eventName} was not triggered.`);
|
||||
// Set up the timeout to reject if the event is not triggered.
|
||||
let timeoutNoEvent;
|
||||
const timeoutProm = new Promise((resolve, reject) => {
|
||||
timeoutNoEvent = setTimeout(() => {
|
||||
console.warn(error);
|
||||
reject(error);
|
||||
}, timeoutDelay);
|
||||
});
|
||||
// Set up the promise to resolve if the event is triggered.
|
||||
const eventProm = makeDeferred();
|
||||
const eventHandler = ev => {
|
||||
if (!predicate || predicate(ev.detail)) {
|
||||
eventProm.resolve();
|
||||
}
|
||||
};
|
||||
messagingBus.addEventListener(eventName, eventHandler);
|
||||
// Start the function expected to trigger the event after the
|
||||
// promise has been registered to not miss any potential event.
|
||||
const funcRes = func();
|
||||
// Make them race (first to resolve/reject wins).
|
||||
await Promise.race([eventProm, timeoutProm]).finally(() => {
|
||||
// Execute clean up regardless of whether the promise is
|
||||
// rejected or not.
|
||||
clearTimeout(timeoutNoEvent);
|
||||
messagingBus.removeEventListener(eventName, eventHandler);
|
||||
});
|
||||
// If the event is triggered before the end of the async function,
|
||||
// ensure the function finishes its job before returning.
|
||||
return await funcRes;
|
||||
};
|
||||
}
|
||||
|
||||
function getClick({ afterNextRender }) {
|
||||
return async function click(selector) {
|
||||
await afterNextRender(() => {
|
||||
if (typeof selector === "string") {
|
||||
$(selector)[0].click();
|
||||
} else if (selector instanceof HTMLElement) {
|
||||
selector.click();
|
||||
} else {
|
||||
// jquery
|
||||
selector[0].click();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getMouseenter({ afterNextRender }) {
|
||||
return async function mouseenter(selector) {
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(selector).dispatchEvent(new window.MouseEvent('mouseenter'))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getOpenDiscuss(afterEvent, webClient, { context = {}, params, ...props } = {}) {
|
||||
return async function openDiscuss({ waitUntilMessagesLoaded = true } = {}) {
|
||||
const actionOpenDiscuss = {
|
||||
// hardcoded actionId, required for discuss_container props validation.
|
||||
id: 104,
|
||||
context,
|
||||
params,
|
||||
tag: 'mail.action_discuss',
|
||||
type: 'ir.actions.client',
|
||||
};
|
||||
if (waitUntilMessagesLoaded) {
|
||||
let threadId = context.active_id;
|
||||
if (typeof threadId === 'string') {
|
||||
threadId = parseInt(threadId.split('_')[1]);
|
||||
}
|
||||
return afterNextRender(() => afterEvent({
|
||||
eventName: 'o-thread-view-hint-processed',
|
||||
func: () => doAction(webClient, actionOpenDiscuss, { props }),
|
||||
message: "should wait until discuss loaded its messages",
|
||||
predicate: ({ hint, threadViewer }) => {
|
||||
return (
|
||||
hint.type === 'messages-loaded' &&
|
||||
(!threadId || threadViewer.thread.id === threadId)
|
||||
);
|
||||
},
|
||||
}));
|
||||
}
|
||||
return afterNextRender(() => doAction(webClient, actionOpenDiscuss, { props }));
|
||||
};
|
||||
}
|
||||
|
||||
function getOpenFormView(afterEvent, openView) {
|
||||
return async function openFormView(action, { props, waitUntilDataLoaded = true, waitUntilMessagesLoaded = true } = {}) {
|
||||
action['views'] = [[false, 'form']];
|
||||
const func = () => openView(action, props);
|
||||
const waitData = func => afterNextRender(() => afterEvent({
|
||||
eventName: 'o-thread-loaded-data',
|
||||
func,
|
||||
message: "should wait until chatter loaded its data",
|
||||
predicate: ({ thread }) => {
|
||||
return (
|
||||
thread.model === action.res_model &&
|
||||
thread.id === action.res_id
|
||||
);
|
||||
},
|
||||
}));
|
||||
const waitMessages = func => afterNextRender(() => afterEvent({
|
||||
eventName: 'o-thread-loaded-messages',
|
||||
func,
|
||||
message: "should wait until chatter loaded its messages",
|
||||
predicate: ({ thread }) => {
|
||||
return (
|
||||
thread.model === action.res_model &&
|
||||
thread.id === action.res_id
|
||||
);
|
||||
},
|
||||
}));
|
||||
if (waitUntilDataLoaded && waitUntilMessagesLoaded) {
|
||||
return waitData(() => waitMessages(func));
|
||||
}
|
||||
if (waitUntilDataLoaded) {
|
||||
return waitData(func);
|
||||
}
|
||||
if (waitUntilMessagesLoaded) {
|
||||
return waitMessages(func);
|
||||
}
|
||||
return func();
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public: start function helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Main function used to make a mocked environment with mocked messaging env.
|
||||
*
|
||||
* @param {Object} [param0={}]
|
||||
* @param {Object} [param0.serverData] The data to pass to the webClient
|
||||
* @param {Object} [param0.discuss={}] provide data that is passed to the discuss action.
|
||||
* @param {Object} [param0.legacyServices]
|
||||
* @param {Object} [param0.services]
|
||||
* @param {function} [param0.mockRPC]
|
||||
* @param {boolean} [param0.hasTimeControl=false] if set, all flow of time
|
||||
* with `messaging.browser.setTimeout` are fully controlled by test itself.
|
||||
* @param {integer} [param0.loadingBaseDelayDuration=0]
|
||||
* @param {Deferred|Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()]
|
||||
* Deferred that let tests block messaging creation and simulate resolution.
|
||||
* Useful for testing working components when messaging is not yet created.
|
||||
* @param {string} [param0.waitUntilMessagingCondition='initialized'] Determines
|
||||
* the condition of messaging when this function is resolved.
|
||||
* Supported values: ['none', 'created', 'initialized'].
|
||||
* - 'none': the function resolves regardless of whether messaging is created.
|
||||
* - 'created': the function resolves when messaging is created, but
|
||||
* regardless of whether messaging is initialized.
|
||||
* - 'initialized' (default): the function resolves when messaging is
|
||||
* initialized.
|
||||
* To guarantee messaging is not created, test should pass a pending deferred
|
||||
* as param of `messagingBeforeCreationDeferred`. To make sure messaging is
|
||||
* not initialized, test should mock RPC `mail/init_messaging` and block its
|
||||
* resolution.
|
||||
* @throws {Error} in case some provided parameters are wrong, such as
|
||||
* `waitUntilMessagingCondition`.
|
||||
* @returns {Object}
|
||||
*/
|
||||
async function start(param0 = {}) {
|
||||
// patch _.debounce and _.throttle to be fast and synchronous.
|
||||
patchWithCleanup(_, {
|
||||
debounce: func => func,
|
||||
throttle: func => func,
|
||||
});
|
||||
const {
|
||||
discuss = {},
|
||||
hasTimeControl,
|
||||
waitUntilMessagingCondition = 'initialized',
|
||||
} = param0;
|
||||
const advanceTime = hasTimeControl ? getAdvanceTime() : undefined;
|
||||
const target = param0['target'] || getFixture();
|
||||
param0['target'] = target;
|
||||
if (!['none', 'created', 'initialized'].includes(waitUntilMessagingCondition)) {
|
||||
throw Error(`Unknown parameter value ${waitUntilMessagingCondition} for 'waitUntilMessaging'.`);
|
||||
}
|
||||
const messagingBus = new EventBus();
|
||||
const afterEvent = getAfterEvent({ messagingBus });
|
||||
|
||||
const pyEnv = await getPyEnv();
|
||||
param0.serverData = param0.serverData || getActionManagerServerData();
|
||||
param0.serverData.models = { ...pyEnv.getData(), ...param0.serverData.models };
|
||||
param0.serverData.views = { ...pyEnv.getViews(), ...param0.serverData.views };
|
||||
let webClient;
|
||||
await afterNextRender(async () => {
|
||||
webClient = await getWebClientReady({ ...param0, messagingBus });
|
||||
if (waitUntilMessagingCondition === 'created') {
|
||||
await webClient.env.services.messaging.modelManager.messagingCreatedPromise;
|
||||
}
|
||||
if (waitUntilMessagingCondition === 'initialized') {
|
||||
await webClient.env.services.messaging.modelManager.messagingCreatedPromise;
|
||||
await webClient.env.services.messaging.modelManager.messagingInitializedPromise;
|
||||
}
|
||||
});
|
||||
|
||||
registerCleanup(async () => {
|
||||
await webClient.env.services.messaging.modelManager.messagingInitializedPromise;
|
||||
webClient.env.services.messaging.modelManager.destroy();
|
||||
delete webClient.env.services.messaging;
|
||||
delete owl.Component.env.services.messaging;
|
||||
delete owl.Component.env[wowlServicesSymbol].messaging;
|
||||
delete owl.Component.env;
|
||||
});
|
||||
const openView = async (action, options) => {
|
||||
action['type'] = action['type'] || 'ir.actions.act_window';
|
||||
await afterNextRender(() => doAction(webClient, action, { props: options }));
|
||||
};
|
||||
return {
|
||||
advanceTime,
|
||||
afterEvent,
|
||||
afterNextRender,
|
||||
click: getClick({ afterNextRender }),
|
||||
env: webClient.env,
|
||||
insertText,
|
||||
messaging: webClient.env.services.messaging.modelManager.messaging,
|
||||
mouseenter: getMouseenter({ afterNextRender }),
|
||||
openDiscuss: getOpenDiscuss(afterEvent, webClient, discuss),
|
||||
openView,
|
||||
openFormView: getOpenFormView(afterEvent, openView),
|
||||
pyEnv,
|
||||
webClient,
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public: file utilities
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Drag some files over a DOM element
|
||||
*
|
||||
* @param {DOM.Element} el
|
||||
* @param {Object[]} file must have been create beforehand
|
||||
* @see testUtils.file.createFile
|
||||
*/
|
||||
function dragenterFiles(el, files) {
|
||||
const ev = new Event('dragenter', { bubbles: true });
|
||||
Object.defineProperty(ev, 'dataTransfer', {
|
||||
value: _createFakeDataTransfer(files),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop some files on a DOM element
|
||||
*
|
||||
* @param {DOM.Element} el
|
||||
* @param {Object[]} files must have been created beforehand
|
||||
* @see testUtils.file.createFile
|
||||
*/
|
||||
function dropFiles(el, files) {
|
||||
const ev = new Event('drop', { bubbles: true });
|
||||
Object.defineProperty(ev, 'dataTransfer', {
|
||||
value: _createFakeDataTransfer(files),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste some files on a DOM element
|
||||
*
|
||||
* @param {DOM.Element} el
|
||||
* @param {Object[]} files must have been created beforehand
|
||||
* @see testUtils.file.createFile
|
||||
*/
|
||||
function pasteFiles(el, files) {
|
||||
const ev = new Event('paste', { bubbles: true });
|
||||
Object.defineProperty(ev, 'clipboardData', {
|
||||
value: _createFakeDataTransfer(files),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public: input utilities
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} content
|
||||
*/
|
||||
async function insertText(selector, content) {
|
||||
await afterNextRender(() => {
|
||||
document.querySelector(selector).focus();
|
||||
for (const char of content) {
|
||||
document.execCommand('insertText', false, char);
|
||||
document.querySelector(selector).dispatchEvent(new window.KeyboardEvent('keydown', { key: char }));
|
||||
document.querySelector(selector).dispatchEvent(new window.KeyboardEvent('keyup', { key: char }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public: DOM utilities
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determine if a DOM element has been totally scrolled
|
||||
*
|
||||
* A 1px margin of error is given to accomodate subpixel rounding issues and
|
||||
* Element.scrollHeight value being either int or decimal
|
||||
*
|
||||
* @param {DOM.Element} el
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isScrolledToBottom(el) {
|
||||
return Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) <= 1;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Export
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
afterNextRender,
|
||||
dragenterFiles,
|
||||
dropFiles,
|
||||
insertText,
|
||||
isScrolledToBottom,
|
||||
nextAnimationFrame,
|
||||
nextTick,
|
||||
pasteFiles,
|
||||
start,
|
||||
startServer,
|
||||
};
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { nextTick } from '@mail/utils/utils';
|
||||
|
||||
import { browser } from '@web/core/browser/browser';
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
|
||||
export function getAdvanceTime() {
|
||||
// list of timeout ids that have timed out.
|
||||
let timedOutIds = [];
|
||||
// key: timeoutId, value: func + remaining duration
|
||||
const timeouts = new Map();
|
||||
patchWithCleanup(browser, {
|
||||
clearTimeout: id => {
|
||||
timeouts.delete(id);
|
||||
timedOutIds = timedOutIds.filter(i => i !== id);
|
||||
},
|
||||
setTimeout: (func, duration) => {
|
||||
const timeoutId = _.uniqueId('timeout_');
|
||||
const timeout = {
|
||||
id: timeoutId,
|
||||
isTimedOut: false,
|
||||
func,
|
||||
duration,
|
||||
};
|
||||
timeouts.set(timeoutId, timeout);
|
||||
if (duration === 0) {
|
||||
timedOutIds.push(timeoutId);
|
||||
timeout.isTimedOut = true;
|
||||
}
|
||||
return timeoutId;
|
||||
},
|
||||
});
|
||||
return async function (duration) {
|
||||
await nextTick();
|
||||
for (const id of timeouts.keys()) {
|
||||
const timeout = timeouts.get(id);
|
||||
if (timeout.isTimedOut) {
|
||||
continue;
|
||||
}
|
||||
timeout.duration = Math.max(timeout.duration - duration, 0);
|
||||
if (timeout.duration === 0) {
|
||||
timedOutIds.push(id);
|
||||
}
|
||||
}
|
||||
while (timedOutIds.length > 0) {
|
||||
const id = timedOutIds.shift();
|
||||
const timeout = timeouts.get(id);
|
||||
timeouts.delete(id);
|
||||
timeout.func();
|
||||
await nextTick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { busService } from '@bus/services/bus_service';
|
||||
import { imStatusService } from '@bus/im_status_service';
|
||||
import { multiTabService } from '@bus/multi_tab_service';
|
||||
import { makeMultiTabToLegacyEnv } from '@bus/services/legacy/make_multi_tab_to_legacy_env';
|
||||
import { makeBusServiceToLegacyEnv } from '@bus/services/legacy/make_bus_service_to_legacy_env';
|
||||
import { makeFakePresenceService } from '@bus/../tests/helpers/mock_services';
|
||||
|
||||
import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container';
|
||||
import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container';
|
||||
import { DiscussContainer } from '@mail/components/discuss_container/discuss_container';
|
||||
import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container';
|
||||
import { messagingService } from '@mail/services/messaging_service';
|
||||
import { systrayService } from '@mail/services/systray_service';
|
||||
import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env';
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { createWebClient } from "@web/../tests/webclient/helpers";
|
||||
|
||||
const ROUTES_TO_IGNORE = [
|
||||
'/web/webclient/load_menus',
|
||||
'/web/dataset/call_kw/res.users/load_views',
|
||||
'/web/dataset/call_kw/res.users/systray_get_activities'
|
||||
];
|
||||
const WEBCLIENT_PARAMETER_NAMES = new Set(['legacyParams', 'mockRPC', 'serverData', 'target', 'webClientClass']);
|
||||
const SERVICES_PARAMETER_NAMES = new Set([
|
||||
'legacyServices', 'loadingBaseDelayDuration', 'messagingBeforeCreationDeferred',
|
||||
'messagingBus', 'services',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Add required components to the main component registry.
|
||||
*/
|
||||
function setupMainComponentRegistry() {
|
||||
const mainComponentRegistry = registry.category('main_components');
|
||||
mainComponentRegistry.add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer });
|
||||
mainComponentRegistry.add('DialogManagerContainer', { Component: DialogManagerContainer });
|
||||
registry.category('actions').add('mail.action_discuss', DiscussContainer);
|
||||
mainComponentRegistry.add('PopoverManagerContainer', { Component: PopoverManagerContainer });
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup both legacy and new service registries.
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {Object} [param0.services]
|
||||
* @param {number} [param0.loadingBaseDelayDuration=0]
|
||||
* @param {Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()]
|
||||
* Deferred that let tests block messaging creation and simulate resolution.
|
||||
* Useful for testing components behavior when messaging is not yet created.
|
||||
* @param {EventBus} [param0.messagingBus]
|
||||
* @returns {LegacyRegistry} The registry containing all the legacy services that will be passed
|
||||
* to the webClient as a legacy parameter.
|
||||
*/
|
||||
function setupMessagingServiceRegistries({
|
||||
loadingBaseDelayDuration = 0,
|
||||
messagingBeforeCreationDeferred = Promise.resolve(),
|
||||
messagingBus,
|
||||
services,
|
||||
}) {
|
||||
const serviceRegistry = registry.category('services');
|
||||
|
||||
patchWithCleanup(messagingService, {
|
||||
async _startModelManager(modelManager, messagingValues) {
|
||||
modelManager.isDebug = true;
|
||||
const _super = this._super.bind(this);
|
||||
await messagingBeforeCreationDeferred;
|
||||
return _super(modelManager, messagingValues);
|
||||
},
|
||||
});
|
||||
|
||||
const messagingValues = {
|
||||
start() {
|
||||
return {
|
||||
isInQUnitTest: true,
|
||||
disableAnimation: true,
|
||||
loadingBaseDelayDuration,
|
||||
messagingBus,
|
||||
userNotificationManager: { canPlayAudio: false },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
services = {
|
||||
bus_service: busService,
|
||||
im_status: imStatusService,
|
||||
messaging: messagingService,
|
||||
messagingValues,
|
||||
presence: makeFakePresenceService({
|
||||
isOdooFocused: () => true,
|
||||
}),
|
||||
systrayService,
|
||||
multi_tab: multiTabService,
|
||||
...services,
|
||||
};
|
||||
|
||||
Object.entries(services).forEach(([serviceName, service]) => {
|
||||
serviceRegistry.add(serviceName, service);
|
||||
});
|
||||
|
||||
registry.category('wowlToLegacyServiceMappers').add('bus_service_to_legacy_env', makeBusServiceToLegacyEnv);
|
||||
registry.category('wowlToLegacyServiceMappers').add('multi_tab_to_legacy_env', makeMultiTabToLegacyEnv);
|
||||
registry.category('wowlToLegacyServiceMappers').add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a properly configured instance of WebClient, with the messaging service and all it's
|
||||
* dependencies initialized.
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {Object} [param0.serverData]
|
||||
* @param {Object} [param0.services]
|
||||
* @param {Object} [param0.loadingBaseDelayDuration]
|
||||
* @param {Object} [param0.messagingBeforeCreationDeferred]
|
||||
* @param {EventBus} [param0.messagingBus] The event bus to be used by messaging.
|
||||
* @returns {WebClient}
|
||||
*/
|
||||
async function getWebClientReady(param0) {
|
||||
setupMainComponentRegistry();
|
||||
|
||||
const servicesParameters = {};
|
||||
const param0Entries = Object.entries(param0);
|
||||
for (const [parameterName, value] of param0Entries) {
|
||||
if (SERVICES_PARAMETER_NAMES.has(parameterName)) {
|
||||
servicesParameters[parameterName] = value;
|
||||
}
|
||||
}
|
||||
setupMessagingServiceRegistries(servicesParameters);
|
||||
|
||||
const webClientParameters = {};
|
||||
for (const [parameterName, value] of param0Entries) {
|
||||
if (WEBCLIENT_PARAMETER_NAMES.has(parameterName)) {
|
||||
webClientParameters[parameterName] = value;
|
||||
}
|
||||
}
|
||||
return createWebClient(webClientParameters);
|
||||
}
|
||||
|
||||
export {
|
||||
getWebClientReady,
|
||||
ROUTES_TO_IGNORE,
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { InputPlugin } from "@html_editor/core/input_plugin";
|
||||
import { MentionPlugin } from "@mail/views/web/fields/html_composer_message_field/mention_plugin";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
describe("Implicit plugin dependencies", () => {
|
||||
test("position as an implicit dependency", async () => {
|
||||
for (const P of [MentionPlugin]) {
|
||||
// input dependency through the "beforeinput_handlers" and
|
||||
// "input_handlers" resources. This dependency was added because the
|
||||
// plugin is heavily dependent on inputs handling and will appear
|
||||
// broken without the appropriate handlers.
|
||||
expect(P.dependencies).toInclude(InputPlugin.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,157 @@
|
|||
import { setSelection } from "@html_editor/../tests/_helpers/selection";
|
||||
import { insertText } from "@html_editor/../tests/_helpers/user_actions";
|
||||
import { expectElementCount } from "@html_editor/../tests/_helpers/ui_expectations";
|
||||
import { HtmlMailField } from "@mail/views/web/fields/html_mail_field/html_mail_field";
|
||||
import { after, before, beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { press, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, enableTransitions } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { mailModels } from "../mail_test_helpers";
|
||||
|
||||
function setSelectionInHtmlField(selector = "p", fieldName = "body") {
|
||||
const anchorNode = queryOne(`[name='${fieldName}'] .odoo-editor-editable ${selector}`);
|
||||
setSelection({ anchorNode, anchorOffset: 0 });
|
||||
return anchorNode;
|
||||
}
|
||||
|
||||
function useCustomStyleRules(rules = "") {
|
||||
let style;
|
||||
before(() => {
|
||||
style = document.createElement("STYLE");
|
||||
style.type = "text/css";
|
||||
style.append(document.createTextNode(rules));
|
||||
document.head.append(style);
|
||||
});
|
||||
after(() => {
|
||||
style.remove();
|
||||
});
|
||||
}
|
||||
|
||||
class CustomMessage extends models.Model {
|
||||
_name = "custom.message";
|
||||
|
||||
title = fields.Char();
|
||||
body = fields.Html();
|
||||
|
||||
_records = [
|
||||
{ id: 1, title: "first", body: "<p>first</p>" },
|
||||
{ id: 2, title: "second", body: "<p>second</p>" },
|
||||
];
|
||||
|
||||
_onChanges = {
|
||||
title(record) {
|
||||
record.body = `<p>${record.title}</p>`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
defineModels({ ...mailModels, CustomMessage });
|
||||
|
||||
let htmlEditor;
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(HtmlMailField.prototype, {
|
||||
onEditorLoad(editor) {
|
||||
htmlEditor = editor;
|
||||
return super.onEditorLoad(...arguments);
|
||||
},
|
||||
getConfig() {
|
||||
const config = super.getConfig();
|
||||
config.Plugins = config.Plugins.filter((Plugin) => Plugin.id !== "editorVersion");
|
||||
return config;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("HtmlMail save inline html", async function () {
|
||||
enableTransitions();
|
||||
useCustomStyleRules(`.test-h1-inline .note-editable h1 { color: #111827 !important; }`);
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].body.replace(/font-size: ?(\d+(\.\d+)?)px/, "font-size: []px")).toBe(
|
||||
`<h1 style="border-radius:0px;border-style:none;padding:0px;margin:0px 0 8px 0;box-sizing:border-box;border-left-color:#111827;border-bottom-color:#111827;border-right-color:#111827;border-top-color:#111827;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;font-size: []px;color:#111827;line-height:1.2;font-weight:500;font-family:'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, 'Noto Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';">first</h1>`
|
||||
);
|
||||
expect.step("web_save");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "custom.message",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="body" widget="html_mail" class="test-h1-inline"/>
|
||||
</form>`,
|
||||
});
|
||||
setSelectionInHtmlField();
|
||||
await insertText(htmlEditor, "/heading1");
|
||||
await press("enter");
|
||||
expect(".odoo-editor-editable").toHaveInnerHTML("<h1> first </h1>");
|
||||
|
||||
await clickSave();
|
||||
await expect.waitForSteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("HtmlMail don't have access to column commands", async function () {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "custom.message",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="body" widget="html_mail"/>
|
||||
</form>`,
|
||||
});
|
||||
setSelectionInHtmlField();
|
||||
await insertText(htmlEditor, "/");
|
||||
await animationFrame();
|
||||
await expectElementCount(".o-we-powerbox", 1);
|
||||
|
||||
await insertText(htmlEditor, "column");
|
||||
await animationFrame();
|
||||
await expectElementCount(".o-we-powerbox", 0);
|
||||
});
|
||||
|
||||
test("HtmlMail add icon and save inline html", async function () {
|
||||
enableTransitions();
|
||||
useCustomStyleRules(
|
||||
`.test-icon-inline .note-editable .fa {
|
||||
color: rgb(55,65,81) !important;
|
||||
background-color: rgb(249,250,251) !important;
|
||||
}
|
||||
p, img {
|
||||
border-color: #ff0000 !important;
|
||||
}
|
||||
`
|
||||
);
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].body).toBe(
|
||||
`<p style="border-radius:0px;border-style:none;padding:0px;margin:0px 0 16px 0;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;border-left-color:#ff0000;border-bottom-color:#ff0000;border-right-color:#ff0000;border-top-color:#ff0000;"><span style="display: inline-block; width: 14px; height: 14px; vertical-align: text-bottom;" class="oe_unbreakable "><img width="14" height="14" src="/mail/font_to_img/61440/rgb(55%2C65%2C81)/rgb(249%2C250%2C251)/14x14" data-class="fa fa-glass" data-style="null" style="border-radius:0px;border-style:none;padding:0px;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;border-left-color:#ff0000;border-bottom-color:#ff0000;border-right-color:#ff0000;border-top-color:#ff0000;box-sizing: border-box; line-height: 14px; width: 14px; height: 14px; vertical-align: unset; margin: 0px;"></span>first</p>`
|
||||
);
|
||||
expect.step("web_save");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "custom.message",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="body" widget="html_mail" class="test-icon-inline"/>
|
||||
</form>`,
|
||||
});
|
||||
setSelectionInHtmlField();
|
||||
await insertText(htmlEditor, "/image");
|
||||
await press("enter");
|
||||
|
||||
await contains("a.nav-link:contains('Icons')").click();
|
||||
await contains("span.fa-glass").click();
|
||||
|
||||
await clickSave();
|
||||
await expect.waitForSteps(["web_save"]);
|
||||
});
|
||||
149
odoo-bringout-oca-ocb-mail/mail/static/tests/inline/utils.js
Normal file
149
odoo-bringout-oca-ocb-mail/mail/static/tests/inline/utils.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import {
|
||||
TABLE_ATTRIBUTES,
|
||||
TABLE_STYLES,
|
||||
} from "@mail/views/web/fields/html_mail_field/convert_inline";
|
||||
|
||||
const tableAttributesString = Object.keys(TABLE_ATTRIBUTES)
|
||||
.map((key) => `${key}="${TABLE_ATTRIBUTES[key]}"`)
|
||||
.join(" ");
|
||||
const tableStylesString = Object.keys(TABLE_STYLES)
|
||||
.map((key) => `${key}: ${TABLE_STYLES[key]};`)
|
||||
.join(" ");
|
||||
/**
|
||||
* Take a matrix representing a grid and return an HTML string of the Bootstrap
|
||||
* grid. The matrix is an array of rows, with each row being an array of cells.
|
||||
* Each cell can be represented either by a 0 < number < 13 (col-#) or a falsy
|
||||
* value (col). Each cell has its coordinates `(row index, column index)` as
|
||||
* text content.
|
||||
* Eg: [ // <div class="container">
|
||||
* [ // <div class="row">
|
||||
* 1, // <div class="col-1">(0, 0)</div>
|
||||
* 11, // <div class="col-11">(0, 1)</div>
|
||||
* ], // </div>
|
||||
* [ // <div class="row">
|
||||
* false, // <div class="col">(1, 0)</div>
|
||||
* ], // </div>
|
||||
* ] // </div>
|
||||
*
|
||||
* @param {Array<Array<Number|null>>} matrix
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGridHtml(matrix) {
|
||||
return (
|
||||
`<div class="container">` +
|
||||
matrix
|
||||
.map(
|
||||
(row, iRow) =>
|
||||
`<div class="row">` +
|
||||
row
|
||||
.map(
|
||||
(col, iCol) =>
|
||||
`<div class="${
|
||||
col ? "col-" + col : "col"
|
||||
}">(${iRow}, ${iCol})</div>`
|
||||
)
|
||||
.join("") +
|
||||
`</div>`
|
||||
)
|
||||
.join("") +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
export function getTdHtml(colspan, text, containerWidth) {
|
||||
const style = containerWidth
|
||||
? ` style="max-width: ${Math.round(((containerWidth * colspan) / 12) * 100) / 100}px;"`
|
||||
: "";
|
||||
return `<td colspan="${colspan}"${style}>${text}</td>`;
|
||||
}
|
||||
/**
|
||||
* Take a matrix representing a table and return an HTML string of the table.
|
||||
* The matrix is an array of rows, with each row being an array of cells. Each
|
||||
* cell is represented by a tuple of numbers [colspan, width (in percent)]. A
|
||||
* cell can have a string as third value to represent its text content. The
|
||||
* default text content of each cell is its coordinates `(row index, column
|
||||
* index)`. If the cell has a number as third value, it will be used as the
|
||||
* max-width of the cell (in pixels).
|
||||
* Eg: [ // <table> (note: extra attrs and styles apply)
|
||||
* [ // <tr>
|
||||
* [1, 8], // <td colspan="1" width="8%">(0, 0)</td>
|
||||
* [11, 92] // <td colspan="11" width="92%">(0, 1)</td>
|
||||
* ], // </tr>
|
||||
* [ // <tr>
|
||||
* [2, 17, 'A'], // <td colspan="2" width="17%">A</td>
|
||||
* [10, 83], // <td colspan="10" width="83%">(1, 1)</td>
|
||||
* ], // </tr>
|
||||
* ] // </table>
|
||||
*
|
||||
* @param {Array<Array<Array<[Number, Number, string?, number?]>>>} matrix
|
||||
* @param {Number} [containerWidth]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getTableHtml(matrix, containerWidth) {
|
||||
return (
|
||||
`<table ${tableAttributesString} style="width: 100% !important; ${tableStylesString}">` +
|
||||
matrix
|
||||
.map(
|
||||
(row, iRow) =>
|
||||
`<tr>` +
|
||||
row
|
||||
.map((col, iCol) =>
|
||||
getTdHtml(
|
||||
col[0],
|
||||
typeof col[2] === "string" ? col[2] : `(${iRow}, ${iCol})`,
|
||||
containerWidth
|
||||
)
|
||||
)
|
||||
.join("") +
|
||||
`</tr>`
|
||||
)
|
||||
.join("") +
|
||||
`</table>`
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Take a number of rows and a number of columns (or number of columns per
|
||||
* individual row) and return an HTML string of the corresponding grid. Every
|
||||
* column is a regular Bootstrap "col" (no col-#).
|
||||
* Eg: [2, 3] <=> getGridHtml([[false, false, false], [false, false, false]])
|
||||
* Eg: [2, [2, 1]] <=> getGridHtml([[false, false], [false]])
|
||||
*
|
||||
* @see getGridHtml
|
||||
* @param {Number} nRows
|
||||
* @param {Number|Number[]} nCols
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRegularGridHtml(nRows, nCols) {
|
||||
const matrix = new Array(nRows)
|
||||
.fill()
|
||||
.map((_, iRow) => new Array(Array.isArray(nCols) ? nCols[iRow] : nCols).fill());
|
||||
return getGridHtml(matrix);
|
||||
}
|
||||
/**
|
||||
* Take a number of rows, a number of columns (or number of columns per
|
||||
* individual row), a colspan (or colspan per individual row) and a width (or
|
||||
* width per individual row, in percent), and return an HTML string of the
|
||||
* corresponding table. Every cell in a row has the same colspan/width.
|
||||
* Eg: [2, 2, 6, 50] <=> getTableHtml([[[6, 50], [6, 50]], [[6, 50], [6, 50]]])
|
||||
* Eg: [2, [2, 1], [6, 12], [50, 100]] <=> getTableHtml([[[6, 50], [6, 50]], [[12, 100]]])
|
||||
*
|
||||
* @see getTableHtml
|
||||
* @param {Number} nRows
|
||||
* @param {Number|Number[]} nCols
|
||||
* @param {Number|Number[]} colspan
|
||||
* @param {Number|Number[]} width
|
||||
* @param {Number} containerWidth
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRegularTableHtml(nRows, nCols, colspan, width, containerWidth) {
|
||||
const matrix = new Array(nRows)
|
||||
.fill()
|
||||
.map((_, iRow) =>
|
||||
new Array(Array.isArray(nCols) ? nCols[iRow] : nCols)
|
||||
.fill()
|
||||
.map(() => [
|
||||
Array.isArray(colspan) ? colspan[iRow] : colspan,
|
||||
Array.isArray(width) ? width[iRow] : width,
|
||||
])
|
||||
);
|
||||
return getTableHtml(matrix, containerWidth);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import * as viewHelpers from "@web/../tests/views/helpers";
|
||||
import * as webClientHelpers from "@web/../tests/webclient/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
function registerFakemailPopoutService() {
|
||||
if (!serviceRegistry.contains("mail.popout")) {
|
||||
serviceRegistry.add("mail.popout", {
|
||||
start() {
|
||||
return {
|
||||
get externalWindow() {
|
||||
return null;
|
||||
},
|
||||
popout() {},
|
||||
reset() {},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const superSetupViewRegistries = viewHelpers.setupViewRegistries
|
||||
viewHelpers.setupViewRegistries = () => {
|
||||
registerFakemailPopoutService()
|
||||
return superSetupViewRegistries();
|
||||
}
|
||||
|
||||
const superSetupWebClientRegistries = webClientHelpers.setupWebClientRegistries
|
||||
webClientHelpers.setupWebClientRegistries = () => {
|
||||
registerFakemailPopoutService()
|
||||
return superSetupWebClientRegistries();
|
||||
}
|
||||
|
||||
const superCreateWebClient = webClientHelpers.createWebClient
|
||||
webClientHelpers.createWebClient = (params) => {
|
||||
registerFakemailPopoutService()
|
||||
return superCreateWebClient(params);
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { click, contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers";
|
||||
import { expect, mockTouch, mockUserAgent, queryFirst } from "@odoo/hoot";
|
||||
|
||||
export async function mailCanAddMessageReactionMobile() {
|
||||
mockTouch(true);
|
||||
mockUserAgent("android");
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "Hello world",
|
||||
res_id: channelId,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
body: "Hello Odoo",
|
||||
res_id: channelId,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
await contains(".o-mail-Message:contains('Hello world')");
|
||||
await click(".o-mail-Message:contains('Hello world') [title='Expand']");
|
||||
await click(".o-dropdown-item:contains('Add a Reaction')");
|
||||
await contains(".o-overlay-item:has(.modal .o-EmojiPicker)");
|
||||
const emojiPickerZIndex = parseInt(
|
||||
getComputedStyle(queryFirst(".o-overlay-item:has(.modal .o-EmojiPicker)")).zIndex
|
||||
);
|
||||
const chatWindowZIndex = parseInt(getComputedStyle(queryFirst(".o-mail-ChatWindow")).zIndex);
|
||||
expect(chatWindowZIndex).toBeLessThan(emojiPickerZIndex, {
|
||||
message: "emoji picker modal should be above chat window",
|
||||
});
|
||||
await click(".modal .o-EmojiPicker .o-Emoji:contains('😀')");
|
||||
await contains(".o-mail-MessageReaction:contains('😀')");
|
||||
// Can quickly add new reactions
|
||||
await click(".o-mail-MessageReactions button[title='Add a Reaction']");
|
||||
await click(".modal .o-EmojiPicker .o-Emoji:contains('🤣')");
|
||||
await contains(".o-mail-MessageReaction:contains('🤣')");
|
||||
await contains(".o-mail-MessageReaction:contains('😀')");
|
||||
}
|
||||
|
||||
export async function mailCanCopyTextToClipboardMobile() {
|
||||
mockTouch(true);
|
||||
mockUserAgent("android");
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "Hello world",
|
||||
res_id: channelId,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
{
|
||||
body: "Hello Odoo",
|
||||
res_id: channelId,
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
await contains(".o-mail-Message:contains('Hello world')");
|
||||
await click(".o-mail-Message:contains('Hello world') [title='Expand']");
|
||||
await contains(".o-dropdown-item:contains('Copy to Clipboard')");
|
||||
}
|
||||
1020
odoo-bringout-oca-ocb-mail/mail/static/tests/mail_test_helpers.js
Normal file
1020
odoo-bringout-oca-ocb-mail/mail/static/tests/mail_test_helpers.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,14 @@
|
|||
.o-mail-Discuss-asTabContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
& .o_action_manager {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,984 @@
|
|||
/** @odoo-module alias=@web/../tests/utils default=false */
|
||||
|
||||
import { __debug__, after, afterEach, expect, getFixture } from "@odoo/hoot";
|
||||
import { queryAll, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame, tick } from "@odoo/hoot-mock";
|
||||
import { isMacOS } from "@web/core/browser/feature_detection";
|
||||
import { isVisible } from "@web/core/utils/ui";
|
||||
|
||||
/**
|
||||
* Use `expect.step` instead
|
||||
* @deprecated
|
||||
*/
|
||||
export const step = expect.step;
|
||||
/**
|
||||
* Use `expect.waitForSteps` instead
|
||||
* @deprecated
|
||||
*/
|
||||
export const assertSteps = expect.waitForSteps;
|
||||
|
||||
/** @param {EventInit} [args] */
|
||||
const mapBubblingEvent = (args) => ({ ...args, bubbles: true });
|
||||
|
||||
/** @param {EventInit} [args] */
|
||||
const mapNonBubblingEvent = (args) => ({ ...args, bubbles: false });
|
||||
|
||||
/** @param {EventInit} [args={}] */
|
||||
const mapBubblingPointerEvent = (args = {}) => ({
|
||||
clientX: args.pageX,
|
||||
clientY: args.pageY,
|
||||
...args,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
|
||||
/** @param {EventInit} [args] */
|
||||
const mapNonBubblingPointerEvent = (args) => ({
|
||||
...mapBubblingPointerEvent(args),
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
});
|
||||
|
||||
/** @param {EventInit} [args={}] */
|
||||
const mapCancelableTouchEvent = (args = {}) => ({
|
||||
...args,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
rotation: 0.0,
|
||||
touches: args.touches ? [...args.touches.map((e) => new Touch(e))] : undefined,
|
||||
view: window,
|
||||
zoom: 1.0,
|
||||
});
|
||||
|
||||
/** @param {EventInit} [args] */
|
||||
const mapNonCancelableTouchEvent = (args) => ({
|
||||
...mapCancelableTouchEvent(args),
|
||||
cancelable: false,
|
||||
});
|
||||
|
||||
/** @param {EventInit} [args] */
|
||||
const mapKeyboardEvent = (args) => ({
|
||||
...args,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* @template {typeof Event} T
|
||||
* @param {EventType} eventType
|
||||
* @returns {[T, (attrs: EventInit) => EventInit]}
|
||||
*/
|
||||
const getEventConstructor = (eventType) => {
|
||||
switch (eventType) {
|
||||
// Mouse events
|
||||
case "auxclick":
|
||||
case "click":
|
||||
case "contextmenu":
|
||||
case "dblclick":
|
||||
case "mousedown":
|
||||
case "mouseup":
|
||||
case "mousemove":
|
||||
case "mouseover":
|
||||
case "mouseout": {
|
||||
return [MouseEvent, mapBubblingPointerEvent];
|
||||
}
|
||||
case "mouseenter":
|
||||
case "mouseleave": {
|
||||
return [MouseEvent, mapNonBubblingPointerEvent];
|
||||
}
|
||||
// Pointer events
|
||||
case "pointerdown":
|
||||
case "pointerup":
|
||||
case "pointermove":
|
||||
case "pointerover":
|
||||
case "pointerout": {
|
||||
return [PointerEvent, mapBubblingPointerEvent];
|
||||
}
|
||||
case "pointerenter":
|
||||
case "pointerleave": {
|
||||
return [PointerEvent, mapNonBubblingPointerEvent];
|
||||
}
|
||||
// Focus events
|
||||
case "focusin": {
|
||||
return [FocusEvent, mapBubblingEvent];
|
||||
}
|
||||
case "focus":
|
||||
case "blur": {
|
||||
return [FocusEvent, mapNonBubblingEvent];
|
||||
}
|
||||
// Clipboard events
|
||||
case "cut":
|
||||
case "copy":
|
||||
case "paste": {
|
||||
return [ClipboardEvent, mapBubblingEvent];
|
||||
}
|
||||
// Keyboard events
|
||||
case "keydown":
|
||||
case "keypress":
|
||||
case "keyup": {
|
||||
return [KeyboardEvent, mapKeyboardEvent];
|
||||
}
|
||||
// Drag events
|
||||
case "drag":
|
||||
case "dragend":
|
||||
case "dragenter":
|
||||
case "dragstart":
|
||||
case "dragleave":
|
||||
case "dragover":
|
||||
case "drop": {
|
||||
return [DragEvent, mapBubblingEvent];
|
||||
}
|
||||
// Input events
|
||||
case "input": {
|
||||
return [InputEvent, mapBubblingEvent];
|
||||
}
|
||||
// Composition events
|
||||
case "compositionstart":
|
||||
case "compositionend": {
|
||||
return [CompositionEvent, mapBubblingEvent];
|
||||
}
|
||||
// UI events
|
||||
case "scroll": {
|
||||
return [UIEvent, mapNonBubblingEvent];
|
||||
}
|
||||
// Touch events
|
||||
case "touchstart":
|
||||
case "touchend":
|
||||
case "touchmove": {
|
||||
return [TouchEvent, mapCancelableTouchEvent];
|
||||
}
|
||||
case "touchcancel": {
|
||||
return [TouchEvent, mapNonCancelableTouchEvent];
|
||||
}
|
||||
// Default: base Event constructor
|
||||
default: {
|
||||
return [Event, mapBubblingEvent];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function findElement(el, selector) {
|
||||
let target = el;
|
||||
if (selector) {
|
||||
const els = el.querySelectorAll(selector);
|
||||
if (els.length === 0) {
|
||||
throw new Error(`No element found (selector: ${selector})`);
|
||||
}
|
||||
if (els.length > 1) {
|
||||
throw new Error(`Found ${els.length} elements, instead of 1 (selector: ${selector})`);
|
||||
}
|
||||
target = els[0];
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {EventType} T
|
||||
* @param {Element} el
|
||||
* @param {string | null | undefined | false} selector
|
||||
* @param {T} eventType
|
||||
* @param {EventInit} [eventInit]
|
||||
* @param {TriggerEventOptions} [options={}]
|
||||
* @returns {GlobalEventHandlersEventMap[T] | Promise<GlobalEventHandlersEventMap[T]>}
|
||||
*/
|
||||
function triggerEvent(el, selector, eventType, eventInit, options = {}) {
|
||||
const errors = [];
|
||||
const target = findElement(el, selector);
|
||||
|
||||
// Error handling
|
||||
if (typeof eventType !== "string") {
|
||||
errors.push("event type must be a string");
|
||||
}
|
||||
if (!target) {
|
||||
errors.push("cannot find target");
|
||||
} else if (!options.skipVisibilityCheck && !isVisible(target)) {
|
||||
errors.push("target is not visible");
|
||||
}
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
`Cannot trigger event${eventType ? ` "${eventType}"` : ""}${
|
||||
selector ? ` (with selector "${selector}")` : ""
|
||||
}: ${errors.join(" and ")}`
|
||||
);
|
||||
}
|
||||
|
||||
// Actual dispatch
|
||||
const [Constructor, processParams] = getEventConstructor(eventType);
|
||||
const event = new Constructor(eventType, processParams(eventInit));
|
||||
target.dispatchEvent(event);
|
||||
|
||||
if (__debug__.debug) {
|
||||
const group = `%c[${event.type.toUpperCase()}]`;
|
||||
console.groupCollapsed(group, "color: #b52c9b");
|
||||
console.log(target, event);
|
||||
console.groupEnd(group, "color: #b52c9b");
|
||||
}
|
||||
|
||||
if (options.sync) {
|
||||
return event;
|
||||
} else {
|
||||
return tick().then(() => event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} el
|
||||
* @param {string | null | undefined | false} selector
|
||||
* @param {(EventType | [EventType, EventInit])[]} [eventDefs]
|
||||
* @param {TriggerEventOptions} [options={}]
|
||||
*/
|
||||
function _triggerEvents(el, selector, eventDefs, options = {}) {
|
||||
const events = [...eventDefs].map((eventDef) => {
|
||||
const [eventType, eventInit] = Array.isArray(eventDef) ? eventDef : [eventDef, {}];
|
||||
return triggerEvent(el, selector, eventType, eventInit, options);
|
||||
});
|
||||
if (options.sync) {
|
||||
return events;
|
||||
} else {
|
||||
return tick().then(() => events);
|
||||
}
|
||||
}
|
||||
|
||||
function _click(
|
||||
el,
|
||||
selector,
|
||||
{ mouseEventInit = {}, skipDisabledCheck = false, skipVisibilityCheck = false } = {}
|
||||
) {
|
||||
if (!skipDisabledCheck && el.disabled) {
|
||||
throw new Error("Can't click on a disabled button");
|
||||
}
|
||||
return _triggerEvents(
|
||||
el,
|
||||
selector,
|
||||
[
|
||||
"pointerdown",
|
||||
"mousedown",
|
||||
"focus",
|
||||
"focusin",
|
||||
"pointerup",
|
||||
"mouseup",
|
||||
["click", mouseEventInit],
|
||||
],
|
||||
{ skipVisibilityCheck }
|
||||
);
|
||||
}
|
||||
|
||||
export async function editInput(el, selector, value) {
|
||||
const input = findElement(el, selector);
|
||||
if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) {
|
||||
throw new Error("Only 'input' and 'textarea' elements can be edited with 'editInput'.");
|
||||
}
|
||||
if (
|
||||
![
|
||||
"text",
|
||||
"textarea",
|
||||
"email",
|
||||
"search",
|
||||
"color",
|
||||
"number",
|
||||
"file",
|
||||
"tel",
|
||||
"range",
|
||||
].includes(input.type)
|
||||
) {
|
||||
throw new Error(`Type "${input.type}" not supported by 'editInput'.`);
|
||||
}
|
||||
|
||||
const eventOpts = {};
|
||||
if (input.type === "file") {
|
||||
const files = Array.isArray(value) ? value : [value];
|
||||
const dataTransfer = new DataTransfer();
|
||||
for (const file of files) {
|
||||
if (!(file instanceof File)) {
|
||||
throw new Error(`File input value should be one or several File objects.`);
|
||||
}
|
||||
dataTransfer.items.add(file);
|
||||
}
|
||||
input.files = dataTransfer.files;
|
||||
eventOpts.skipVisibilityCheck = true;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
await _triggerEvents(input, null, ["input", "change"], eventOpts);
|
||||
|
||||
if (input.type === "file") {
|
||||
// Need to wait for the file to be loaded by the input
|
||||
await tick();
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake object 'dataTransfer', linked to some files,
|
||||
* which is passed to drag and drop events.
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns {Object}
|
||||
*/
|
||||
function createFakeDataTransfer(files) {
|
||||
return {
|
||||
dropEffect: "all",
|
||||
effectAllowed: "all",
|
||||
files,
|
||||
items: [],
|
||||
getData: () => "",
|
||||
types: ["Files"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then clicks on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
* @param {boolean} [options.shiftKey]
|
||||
*/
|
||||
export async function click(selector, options = {}) {
|
||||
const { shiftKey } = options;
|
||||
delete options.shiftKey;
|
||||
await contains(selector, { click: { shiftKey }, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then dragenters `files` on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dragenterFiles(selector, files, options) {
|
||||
await contains(selector, { dragenterFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then dragovers `files` on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dragoverFiles(selector, files, options) {
|
||||
await contains(selector, { dragoverFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then drops `files` on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dropFiles(selector, files, options) {
|
||||
await contains(selector, { dropFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then inputs `files` on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function inputFiles(selector, files, options) {
|
||||
await contains(selector, { inputFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then pastes `files` on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function pasteFiles(selector, files, options) {
|
||||
await contains(selector, { pasteFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then focuses on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function focus(selector, options) {
|
||||
await contains(selector, { setFocus: true, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then inserts the given `content`.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {string} content
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
* @param {boolean} [options.replace=false]
|
||||
*/
|
||||
export async function insertText(selector, content, options = {}) {
|
||||
const { replace = false } = options;
|
||||
delete options.replace;
|
||||
await contains(selector, { ...options, insertText: { content, replace } });
|
||||
await animationFrame(); // wait for t-model synced with new value
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then sets its `scrollTop` to the given value.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {number|"bottom"} scrollTop
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function scroll(selector, scrollTop, options) {
|
||||
await contains(selector, { setScroll: scrollTop, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then triggers `event` on it.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {(import("@web/../tests/helpers/utils").EventType|[import("@web/../tests/helpers/utils").EventType, EventInit])[]} events
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function triggerEvents(selector, events, options) {
|
||||
await contains(selector, { triggerEvents: events, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an hotkey properly disregarding the operating system.
|
||||
*
|
||||
* @param {string} hotkey
|
||||
* @param {boolean} addOverlayModParts
|
||||
* @param {KeyboardEventInit} eventAttrs
|
||||
*/
|
||||
export async function triggerHotkey(hotkey, addOverlayModParts = false, eventAttrs = {}) {
|
||||
eventAttrs.key = hotkey.split("+").pop();
|
||||
if (/shift/i.test(hotkey)) {
|
||||
eventAttrs.shiftKey = true;
|
||||
}
|
||||
if (/control/i.test(hotkey)) {
|
||||
if (isMacOS()) {
|
||||
eventAttrs.metaKey = true;
|
||||
} else {
|
||||
eventAttrs.ctrlKey = true;
|
||||
}
|
||||
}
|
||||
if (/alt/i.test(hotkey) || addOverlayModParts) {
|
||||
if (isMacOS()) {
|
||||
eventAttrs.ctrlKey = true;
|
||||
} else {
|
||||
eventAttrs.altKey = true;
|
||||
}
|
||||
}
|
||||
if (!("bubbles" in eventAttrs)) {
|
||||
eventAttrs.bubbles = true;
|
||||
}
|
||||
const [keydownEvent, keyupEvent] = await _triggerEvents(
|
||||
document.activeElement,
|
||||
null,
|
||||
[
|
||||
["keydown", eventAttrs],
|
||||
["keyup", eventAttrs],
|
||||
],
|
||||
{ skipVisibilityCheck: true }
|
||||
);
|
||||
return { keydownEvent, keyupEvent };
|
||||
}
|
||||
|
||||
function log(ok, message) {
|
||||
expect(Boolean(ok)).toBe(true, { message });
|
||||
}
|
||||
|
||||
let hasUsedContainsPositively = false;
|
||||
afterEach(() => (hasUsedContainsPositively = false));
|
||||
/**
|
||||
* @typedef {[string, ContainsOptions]} ContainsTuple tuple representing params of the contains
|
||||
* function, where the first element is the selector, and the second element is the options param.
|
||||
* @typedef {Object} ContainsOptions
|
||||
* @property {ContainsTuple} [after] if provided, the found element(s) must be after the element
|
||||
* matched by this param.
|
||||
* @property {ContainsTuple} [before] if provided, the found element(s) must be before the element
|
||||
* matched by this param.
|
||||
* @property {Object} [click] if provided, clicks on the first found element
|
||||
* @property {ContainsTuple|ContainsTuple[]} [contains] if provided, the found element(s) must
|
||||
* contain the provided sub-elements.
|
||||
* @property {number} [count=1] numbers of elements to be found to declare the contains check
|
||||
* as successful. Elements are counted after applying all other filters.
|
||||
* @property {Object[]} [dragenterFiles] if provided, dragenters the given files on the found element
|
||||
* @property {Object[]} [dragoverFiles] if provided, dragovers the given files on the found element
|
||||
* @property {Object[]} [dropFiles] if provided, drops the given files on the found element
|
||||
* @property {Object[]} [inputFiles] if provided, inputs the given files on the found element
|
||||
* @property {{content:string, replace:boolean}} [insertText] if provided, adds to (or replace) the
|
||||
* value of the first found element by the given content.
|
||||
* @property {ContainsTuple} [parent] if provided, the found element(s) must have as
|
||||
* parent the node matching the parent parameter.
|
||||
* @property {Object[]} [pasteFiles] if provided, pastes the given files on the found element
|
||||
* @property {number|"bottom"} [scroll] if provided, the scrollTop of the found element(s)
|
||||
* must match.
|
||||
* Note: when using one of the scrollTop options, it is advised to ensure the height is not going
|
||||
* to change soon, by checking with a preceding contains that all the expected elements are in DOM.
|
||||
* @property {boolean} [setFocus] if provided, focuses the first found element.
|
||||
* @property {boolean} [shadowRoot] if provided, targets the shadowRoot of the found elements.
|
||||
* @property {number|"bottom"} [setScroll] if provided, sets the scrollTop on the first found
|
||||
* element.
|
||||
* @property {HTMLElement|OdooEnv} [target=getFixture()]
|
||||
* @property {string[]} [triggerEvents] if provided, triggers the given events on the found element
|
||||
* @property {string} [text] if provided, the textContent of the found element(s) or one of their
|
||||
* descendants must match. Use `textContent` option for a match on the found element(s) only.
|
||||
* @property {string} [textContent] if provided, the textContent of the found element(s) must match.
|
||||
* Prefer `text` option for a match on the found element(s) or any of their descendants, usually
|
||||
* allowing for a simpler and less specific selector.
|
||||
* @property {string} [value] if provided, the input value of the found element(s) must match.
|
||||
* Note: value changes are not observed directly, another mutation must happen to catch them.
|
||||
* @property {boolean} [visible] if provided, the found element(s) must be (in)visible
|
||||
*/
|
||||
class Contains {
|
||||
timeoutCount = 0;
|
||||
/**
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {ContainsOptions} [options={}]
|
||||
*/
|
||||
constructor(selector, options = {}) {
|
||||
this.selector = selector;
|
||||
this.options = options;
|
||||
this.options.count ??= 1;
|
||||
let targetParam;
|
||||
if (this.options.target?.testEnv) {
|
||||
// when OdooEnv, special key `target`. See @start
|
||||
targetParam = this.options.target?.target;
|
||||
}
|
||||
if (!targetParam) {
|
||||
targetParam = this.options.target;
|
||||
}
|
||||
this.options.target = targetParam || getFixture();
|
||||
let selectorMessage = `${this.options.count} of "${this.selector}"`;
|
||||
if (this.options.visible !== undefined) {
|
||||
selectorMessage = `${selectorMessage} ${
|
||||
this.options.visible ? "visible" : "invisible"
|
||||
}`;
|
||||
}
|
||||
if (targetParam) {
|
||||
selectorMessage = `${selectorMessage} inside a specific target`;
|
||||
}
|
||||
if (this.options.parent) {
|
||||
selectorMessage = `${selectorMessage} inside a specific parent`;
|
||||
}
|
||||
if (this.options.contains) {
|
||||
selectorMessage = `${selectorMessage} with a specified sub-contains`;
|
||||
}
|
||||
if (this.options.text !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with text "${this.options.text}"`;
|
||||
}
|
||||
if (this.options.textContent !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with textContent "${this.options.textContent}"`;
|
||||
}
|
||||
if (this.options.value !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with value "${this.options.value}"`;
|
||||
}
|
||||
if (this.options.scroll !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with scroll "${this.options.scroll}"`;
|
||||
}
|
||||
if (this.options.after !== undefined) {
|
||||
selectorMessage = `${selectorMessage} after a specified element`;
|
||||
}
|
||||
if (this.options.before !== undefined) {
|
||||
selectorMessage = `${selectorMessage} before a specified element`;
|
||||
}
|
||||
this.selectorMessage = selectorMessage;
|
||||
if (this.options.contains && !Array.isArray(this.options.contains[0])) {
|
||||
this.options.contains = [this.options.contains];
|
||||
}
|
||||
if (this.options.count) {
|
||||
hasUsedContainsPositively = true;
|
||||
} else if (!hasUsedContainsPositively) {
|
||||
throw new Error(
|
||||
`Starting a test with "contains" of count 0 for selector "${this.selector}" is useless because it might immediately resolve. Start the test by checking that an expected element actually exists.`
|
||||
);
|
||||
}
|
||||
/** @type {string} */
|
||||
this.successMessage = undefined;
|
||||
/** @type {function} */
|
||||
this.executeError = undefined;
|
||||
}
|
||||
|
||||
setTickTimeout() {
|
||||
this.timer = setTimeout(() => {
|
||||
this.timeoutCount++;
|
||||
const res = this.runOnce(
|
||||
`Timeout of ${(this.timeoutCount * this.tickTimeoutDelay) / 1000} seconds`,
|
||||
{ crashOnFail: this.timeoutCount >= 3000 / this.tickTimeoutDelay }
|
||||
);
|
||||
if (!res) {
|
||||
this.setTickTimeout();
|
||||
}
|
||||
}, this.tickTimeoutDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts this contains check, either immediately resolving if there is a
|
||||
* match, or registering appropriate listeners and waiting until there is a
|
||||
* match or a timeout (resolving or rejecting respectively).
|
||||
*
|
||||
* Success or failure messages will be logged with HOOT as well.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
run() {
|
||||
this.done = false;
|
||||
this.def = new Deferred();
|
||||
this.scrollListeners = new Set();
|
||||
this.onBlur = () => this.runOnce("after blur");
|
||||
this.onChange = () => this.runOnce("after change");
|
||||
this.onFocus = () => this.runOnce("after focus");
|
||||
this.onScroll = () => this.runOnce("after scroll");
|
||||
if (!this.runOnce("immediately")) {
|
||||
const hasValue =
|
||||
this.options.value !== undefined ||
|
||||
(typeof this.selector === "string" && this.selector.includes(":value"));
|
||||
this.tickTimeoutDelay = hasValue ? 500 : 3000;
|
||||
this.setTickTimeout();
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
try {
|
||||
this.runOnce("after mutations");
|
||||
} catch (e) {
|
||||
this.def.reject(e); // prevents infinite loop in case of programming error
|
||||
}
|
||||
});
|
||||
this.observer.observe(document.body, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
document.body.addEventListener("blur", this.onBlur, { capture: true });
|
||||
document.body.addEventListener("change", this.onChange, { capture: true });
|
||||
document.body.addEventListener("focus", this.onFocus, { capture: true });
|
||||
after(() => {
|
||||
if (!this.done) {
|
||||
this.runOnce("Test ended", { crashOnFail: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs this contains check once, immediately returning the result (or
|
||||
* undefined), and possibly resolving or rejecting the main promise
|
||||
* (and printing HOOT log) depending on options.
|
||||
* If undefined is returned it means the check was not successful.
|
||||
*
|
||||
* @param {string} whenMessage
|
||||
* @param {Object} [options={}]
|
||||
* @param {boolean} [options.crashOnFail=false]
|
||||
* @param {boolean} [options.executeOnSuccess=true]
|
||||
* @returns {HTMLElement[]|undefined}
|
||||
*/
|
||||
runOnce(whenMessage, { crashOnFail = false, executeOnSuccess = true } = {}) {
|
||||
const res = this.select();
|
||||
if ((res?.length ?? 0) === this.options.count || crashOnFail) {
|
||||
// clean before doing anything else to avoid infinite loop due to side effects
|
||||
this.observer?.disconnect();
|
||||
clearTimeout(this.timer);
|
||||
for (const el of this.scrollListeners ?? []) {
|
||||
el.removeEventListener("scroll", this.onScroll);
|
||||
}
|
||||
document.body.removeEventListener("blur", this.onBlur, { capture: true });
|
||||
document.body.removeEventListener("change", this.onChange, { capture: true });
|
||||
document.body.removeEventListener("focus", this.onFocus, { capture: true });
|
||||
this.done = true;
|
||||
}
|
||||
if ((res?.length ?? 0) === this.options.count) {
|
||||
this.successMessage = `Found ${this.selectorMessage} (${whenMessage})`;
|
||||
if (executeOnSuccess) {
|
||||
this.executeAction(res[0]);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
this.executeError = () => {
|
||||
let message = `Failed to find ${this.selectorMessage} (${whenMessage}).`;
|
||||
message = res
|
||||
? `${message} Found ${res.length} instead.`
|
||||
: `${message} Parent not found.`;
|
||||
if (this.parentContains) {
|
||||
if (this.parentContains.successMessage) {
|
||||
log(true, this.parentContains.successMessage);
|
||||
} else {
|
||||
this.parentContains.executeError();
|
||||
}
|
||||
}
|
||||
log(false, message);
|
||||
this.def?.reject(new Error(message));
|
||||
for (const childContains of this.childrenContains || []) {
|
||||
if (childContains.successMessage) {
|
||||
log(true, childContains.successMessage);
|
||||
} else {
|
||||
childContains.executeError();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (crashOnFail) {
|
||||
this.executeError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the action(s) given to this constructor on the found element,
|
||||
* prints the success messages, and resolves the main deferred.
|
||||
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
executeAction(el) {
|
||||
let message = this.successMessage;
|
||||
if (this.options.click) {
|
||||
message = `${message} and clicked it`;
|
||||
_click(el, undefined, {
|
||||
mouseEventInit: this.options.click,
|
||||
skipDisabledCheck: true,
|
||||
skipVisibilityCheck: true,
|
||||
});
|
||||
}
|
||||
if (this.options.dragenterFiles) {
|
||||
message = `${message} and dragentered ${this.options.dragenterFiles.length} file(s)`;
|
||||
const ev = new Event("dragenter", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dragenterFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.dragoverFiles) {
|
||||
message = `${message} and dragovered ${this.options.dragoverFiles.length} file(s)`;
|
||||
const ev = new Event("dragover", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dragoverFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.dropFiles) {
|
||||
message = `${message} and dropped ${this.options.dropFiles.length} file(s)`;
|
||||
const ev = new Event("drop", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dropFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.inputFiles) {
|
||||
message = `${message} and inputted ${this.options.inputFiles.length} file(s)`;
|
||||
// could not use _createFakeDataTransfer as el.files assignation will only
|
||||
// work with a real FileList object.
|
||||
const dataTransfer = new window.DataTransfer();
|
||||
for (const file of this.options.inputFiles) {
|
||||
dataTransfer.items.add(file);
|
||||
}
|
||||
el.files = dataTransfer.files;
|
||||
/**
|
||||
* Changing files programatically is not supposed to trigger the event but
|
||||
* it does in Chrome versions before 73 (which is on runbot), so in that
|
||||
* case there is no need to make a manual dispatch, because it would lead to
|
||||
* the files being added twice.
|
||||
*/
|
||||
const versionRaw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
||||
const chromeVersion = versionRaw ? parseInt(versionRaw[2], 10) : false;
|
||||
if (!chromeVersion || chromeVersion >= 73) {
|
||||
el.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
if (this.options.insertText !== undefined) {
|
||||
message = `${message} and inserted text "${this.options.insertText.content}" (replace: ${this.options.insertText.replace})`;
|
||||
el.focus();
|
||||
if (this.options.insertText.replace) {
|
||||
el.value = "";
|
||||
el.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Backspace" }));
|
||||
el.dispatchEvent(new window.KeyboardEvent("keyup", { key: "Backspace" }));
|
||||
el.dispatchEvent(new window.InputEvent("input"));
|
||||
}
|
||||
for (const char of this.options.insertText.content) {
|
||||
el.value += char;
|
||||
el.dispatchEvent(new window.KeyboardEvent("keydown", { key: char }));
|
||||
el.dispatchEvent(new window.KeyboardEvent("keyup", { key: char }));
|
||||
el.dispatchEvent(new window.InputEvent("input"));
|
||||
}
|
||||
el.dispatchEvent(new window.InputEvent("change"));
|
||||
}
|
||||
if (this.options.pasteFiles) {
|
||||
message = `${message} and pasted ${this.options.pasteFiles.length} file(s)`;
|
||||
const ev = new Event("paste", { bubbles: true });
|
||||
Object.defineProperty(ev, "clipboardData", {
|
||||
value: createFakeDataTransfer(this.options.pasteFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.setFocus) {
|
||||
message = `${message} and focused it`;
|
||||
el.focus();
|
||||
}
|
||||
if (this.options.setScroll !== undefined) {
|
||||
message = `${message} and set scroll to "${this.options.setScroll}"`;
|
||||
el.scrollTop =
|
||||
this.options.setScroll === "bottom" ? el.scrollHeight : this.options.setScroll;
|
||||
}
|
||||
if (this.options.triggerEvents) {
|
||||
message = `${message} and triggered "${this.options.triggerEvents.join(", ")}" events`;
|
||||
_triggerEvents(el, null, this.options.triggerEvents, {
|
||||
skipVisibilityCheck: true,
|
||||
});
|
||||
}
|
||||
if (this.parentContains) {
|
||||
log(true, this.parentContains.successMessage);
|
||||
}
|
||||
log(true, message);
|
||||
for (const childContains of this.childrenContains) {
|
||||
log(true, childContains.successMessage);
|
||||
}
|
||||
this.def?.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the found element(s) according to this constructor setup.
|
||||
* If undefined is returned it means the parent cannot be found
|
||||
*
|
||||
* @returns {HTMLElement[]|undefined}
|
||||
*/
|
||||
select() {
|
||||
const target = this.selectParent();
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
let elems;
|
||||
if (target === getFixture() && queryFirst(this.selector) === target) {
|
||||
elems = [target];
|
||||
} else {
|
||||
elems = queryAll(this.selector, { root: target });
|
||||
}
|
||||
const baseRes = elems
|
||||
.map((el) => (this.options.shadowRoot ? el.shadowRoot : el))
|
||||
.filter((el) => el);
|
||||
/** @type {Contains[]} */
|
||||
this.childrenContains = [];
|
||||
const res = baseRes.filter((el, currentIndex) => {
|
||||
let condition =
|
||||
(this.options.textContent === undefined ||
|
||||
el.textContent.trim() === this.options.textContent) &&
|
||||
(this.options.value === undefined || el.value === this.options.value) &&
|
||||
(this.options.scroll === undefined ||
|
||||
(this.options.scroll === "bottom"
|
||||
? Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) <= 1
|
||||
: Math.abs(el.scrollTop - this.options.scroll) <= 1));
|
||||
if (condition && this.options.text !== undefined) {
|
||||
if (
|
||||
el.textContent.trim() !== this.options.text &&
|
||||
[...el.querySelectorAll("*")].every(
|
||||
(el) => el.textContent.trim() !== this.options.text
|
||||
)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
}
|
||||
if (condition && this.options.contains) {
|
||||
for (const param of this.options.contains) {
|
||||
const childContains = new Contains(param[0], { ...param[1], target: el });
|
||||
if (
|
||||
!childContains.runOnce(`as child of el ${currentIndex + 1})`, {
|
||||
executeOnSuccess: false,
|
||||
})
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(childContains);
|
||||
}
|
||||
}
|
||||
if (condition && this.options.visible !== undefined) {
|
||||
if (isVisible(el) !== this.options.visible) {
|
||||
condition = false;
|
||||
}
|
||||
}
|
||||
if (condition && this.options.after) {
|
||||
const afterContains = new Contains(this.options.after[0], {
|
||||
...this.options.after[1],
|
||||
target,
|
||||
});
|
||||
const afterEl = afterContains.runOnce(`as "after"`, {
|
||||
executeOnSuccess: false,
|
||||
})?.[0];
|
||||
if (
|
||||
!afterEl ||
|
||||
!(el.compareDocumentPosition(afterEl) & Node.DOCUMENT_POSITION_PRECEDING)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(afterContains);
|
||||
}
|
||||
if (condition && this.options.before) {
|
||||
const beforeContains = new Contains(this.options.before[0], {
|
||||
...this.options.before[1],
|
||||
target,
|
||||
});
|
||||
const beforeEl = beforeContains.runOnce(`as "before"`, {
|
||||
executeOnSuccess: false,
|
||||
})?.[0];
|
||||
if (
|
||||
!beforeEl ||
|
||||
!(el.compareDocumentPosition(beforeEl) & Node.DOCUMENT_POSITION_FOLLOWING)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(beforeContains);
|
||||
}
|
||||
return condition;
|
||||
});
|
||||
if (
|
||||
this.options.scroll !== undefined &&
|
||||
this.scrollListeners &&
|
||||
baseRes.length === this.options.count &&
|
||||
res.length !== this.options.count
|
||||
) {
|
||||
for (const el of baseRes) {
|
||||
if (!this.scrollListeners.has(el)) {
|
||||
this.scrollListeners.add(el);
|
||||
el.addEventListener("scroll", this.onScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the found element that should act as the target (parent) for the
|
||||
* main selector.
|
||||
* If undefined is returned it means the parent cannot be found.
|
||||
*
|
||||
* @returns {HTMLElement|undefined}
|
||||
*/
|
||||
selectParent() {
|
||||
if (this.options.parent) {
|
||||
this.parentContains = new Contains(this.options.parent[0], {
|
||||
...this.options.parent[1],
|
||||
target: this.options.target,
|
||||
});
|
||||
return this.parentContains.runOnce(`as parent`, { executeOnSuccess: false })?.[0];
|
||||
}
|
||||
return this.options.target;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until `count` elements matching the given `selector` are present in
|
||||
* `options.target`.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} selector
|
||||
* @param {ContainsOptions} [options]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function contains(selector, options) {
|
||||
await new Contains(selector, options).run();
|
||||
}
|
||||
222
odoo-bringout-oca-ocb-mail/mail/static/tests/mail_utils.test.js
Normal file
222
odoo-bringout-oca-ocb-mail/mail/static/tests/mail_utils.test.js
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import { addLink, parseAndTransform } from "@mail/utils/common/format";
|
||||
import { useSequential } from "@mail/utils/common/hooks";
|
||||
import {
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "./mail_test_helpers";
|
||||
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("add_link utility function", () => {
|
||||
const testInputs = {
|
||||
"http://admin:password@example.com:8/%2020": true,
|
||||
"https://admin:password@example.com/test": true,
|
||||
"www.example.com:8/test": true,
|
||||
"https://127.0.0.5:8069": true,
|
||||
"www.127.0.0.5": false,
|
||||
"should.notmatch": false,
|
||||
"fhttps://test.example.com/test": false,
|
||||
"https://www.transifex.com/odoo/odoo-11/translate/#fr/lunch?q=text%3A'La+Tartiflette'": true,
|
||||
"https://www.transifex.com/odoo/odoo-11/translate/#fr/$/119303430?q=text%3ATartiflette": true,
|
||||
"https://tenor.com/view/chỗgiặt-dog-smile-gif-13860250": true,
|
||||
"http://www.boîtenoire.be": true,
|
||||
"https://github.com/odoo/enterprise/compare/16.0...odoo-dev:enterprise:16.0-voip-fix_demo_data-tsm?expand=1": true,
|
||||
"https://github.com/odoo/enterprise/compare/16.0...16.0-voip-fix_demo_data-tsm?expand=1": true,
|
||||
"https://github.com/odoo/enterprise/compare/16.0...chỗgiặt-voip-fix_demo_data-tsm?expand=1": true,
|
||||
"https://github.com/odoo/enterprise/compare/chỗgiặt...chỗgiặt-voip-fix_demo_data-tsm?expand=1": true,
|
||||
"https://github.com/odoo/enterprise/compare/@...}-voip-fix_demo_data-tsm?expand=1": true,
|
||||
"https://x.com": true,
|
||||
};
|
||||
|
||||
for (const [content, willLinkify] of Object.entries(testInputs)) {
|
||||
const output = parseAndTransform(content, addLink);
|
||||
if (willLinkify) {
|
||||
expect(output.indexOf("<a ")).toBe(0);
|
||||
expect(output.indexOf("</a>")).toBe(output.length - 4);
|
||||
} else {
|
||||
expect(output.indexOf("<a ")).toBe(-1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("addLink: utility function and special entities", () => {
|
||||
const testInputs = [
|
||||
// textContent not unescaped
|
||||
[
|
||||
markup`<p>https://example.com/?&currency_id</p>`,
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/?&currency_id">https://example.com/?&currency_id</a></p>',
|
||||
],
|
||||
// entities not unescaped
|
||||
[markup`& &amp; > <`, "& &amp; > <"],
|
||||
// > and " not linkified since they are not in URL regex
|
||||
[
|
||||
markup`<p>https://example.com/></p>`,
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/">https://example.com/</a>></p>',
|
||||
],
|
||||
[
|
||||
markup`<p>https://example.com/"hello"></p>`,
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/">https://example.com/</a>"hello"></p>',
|
||||
],
|
||||
// & and ' linkified since they are in URL regex
|
||||
[
|
||||
markup`<p>https://example.com/&hello</p>`,
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/&hello">https://example.com/&hello</a></p>',
|
||||
],
|
||||
[
|
||||
markup`<p>https://example.com/'yeah'</p>`,
|
||||
'<p><a target="_blank" rel="noreferrer noopener" href="https://example.com/\'yeah\'">https://example.com/\'yeah\'</a></p>',
|
||||
],
|
||||
[markup`<p>:'(</p>`, "<p>:'(</p>"],
|
||||
[markup`:'(`, ":'("],
|
||||
["<p>:'(</p>", "<p>:'(</p>"],
|
||||
[":'(", ":'("],
|
||||
[markup`<3`, "<3"],
|
||||
[markup`<3`, "<3"],
|
||||
["<3", "<3"],
|
||||
// Already encoded url should not be encoded twice
|
||||
[
|
||||
markup`https://odoo.com/%5B%5D`,
|
||||
`<a target="_blank" rel="noreferrer noopener" href="https://odoo.com/%5B%5D">https://odoo.com/[]</a>`,
|
||||
],
|
||||
];
|
||||
|
||||
for (const [content, result] of testInputs) {
|
||||
const output = parseAndTransform(content, addLink);
|
||||
expect(output).toBeInstanceOf(markup().constructor);
|
||||
expect(output.toString()).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("addLink: linkify inside text node (1 occurrence)", async () => {
|
||||
const content = markup`<p>some text https://somelink.com</p>`;
|
||||
const linkified = parseAndTransform(content, addLink);
|
||||
expect(linkified.startsWith("<p>some text <a")).toBe(true);
|
||||
expect(linkified.endsWith("</a></p>")).toBe(true);
|
||||
|
||||
// linkify may add some attributes. Since we do not care of their exact
|
||||
// stringified representation, we continue deeper assertion with query
|
||||
// selectors.
|
||||
const fragment = document.createDocumentFragment();
|
||||
const div = document.createElement("div");
|
||||
fragment.appendChild(div);
|
||||
div.innerHTML = linkified;
|
||||
expect(div).toHaveText("some text https://somelink.com");
|
||||
await contains("a", { target: div });
|
||||
expect(div.querySelector(":scope a")).toHaveText("https://somelink.com");
|
||||
});
|
||||
|
||||
test("addLink: linkify inside text node (2 occurrences)", () => {
|
||||
// linkify may add some attributes. Since we do not care of their exact
|
||||
// stringified representation, we continue deeper assertion with query
|
||||
// selectors.
|
||||
const content = markup(
|
||||
"<p>some text https://somelink.com and again https://somelink2.com ...</p>"
|
||||
);
|
||||
const linkified = parseAndTransform(content, addLink);
|
||||
const fragment = document.createDocumentFragment();
|
||||
const div = document.createElement("div");
|
||||
fragment.appendChild(div);
|
||||
div.innerHTML = linkified;
|
||||
expect(div).toHaveText("some text https://somelink.com and again https://somelink2.com ...");
|
||||
expect(div.querySelectorAll(":scope a")).toHaveCount(2);
|
||||
expect(div.querySelectorAll(":scope a")[0]).toHaveText("https://somelink.com");
|
||||
expect(div.querySelectorAll(":scope a")[1]).toHaveText("https://somelink2.com");
|
||||
});
|
||||
|
||||
test("url", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// see: https://www.ietf.org/rfc/rfc1738.txt
|
||||
const messageBody = "https://odoo.com?test=~^|`{}[]#";
|
||||
await insertText(".o-mail-Composer-input", messageBody);
|
||||
await press("Enter");
|
||||
await contains(`.o-mail-Message a:contains(${messageBody})`);
|
||||
});
|
||||
|
||||
test("url with comma at the end", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
const messageBody = "Go to https://odoo.com, it's great!";
|
||||
await insertText(".o-mail-Composer-input", messageBody);
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message a:contains(https://odoo.com)");
|
||||
await contains(`.o-mail-Message-content:contains(${messageBody}`);
|
||||
});
|
||||
|
||||
test("url with dot at the end", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
const messageBody = "Go to https://odoo.com. It's great!";
|
||||
await insertText(".o-mail-Composer-input", messageBody);
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message a:contains(https://odoo.com)");
|
||||
await contains(`.o-mail-Message-content:contains(${messageBody})`);
|
||||
});
|
||||
|
||||
test("url with semicolon at the end", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
const messageBody = "Go to https://odoo.com; it's great!";
|
||||
await insertText(".o-mail-Composer-input", messageBody);
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message a:contains(https://odoo.com)");
|
||||
await contains(`.o-mail-Message-content:contains(${messageBody})`);
|
||||
});
|
||||
|
||||
test("url with ellipsis at the end", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
const messageBody = "Go to https://odoo.com... it's great!";
|
||||
await insertText(".o-mail-Composer-input", messageBody);
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message a:contains(https://odoo.com)");
|
||||
await contains(`.o-mail-Message-content:contains(${messageBody})`);
|
||||
});
|
||||
|
||||
test("url with number in subdomain", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
const messageBody = "https://www.45017478-master-all.runbot134.odoo.com/odoo";
|
||||
await insertText(".o-mail-Composer-input", messageBody);
|
||||
await press("Enter");
|
||||
await contains(
|
||||
".o-mail-Message a:contains(https://www.45017478-master-all.runbot134.odoo.com/odoo)"
|
||||
);
|
||||
});
|
||||
|
||||
test("isSequential doesn't execute intermediate call.", async () => {
|
||||
const sequential = useSequential();
|
||||
let index = 0;
|
||||
const sequence = () => {
|
||||
index++;
|
||||
const i = index;
|
||||
return sequential(async () => {
|
||||
expect.step(i.toString());
|
||||
return new Promise((r) => setTimeout(() => r(i), 1));
|
||||
});
|
||||
};
|
||||
const result = await Promise.all([sequence(), sequence(), sequence(), sequence(), sequence()]);
|
||||
expect(result).toEqual([1, undefined, undefined, undefined, 5]);
|
||||
expect.verifySteps(["1", "5"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,514 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
hover,
|
||||
insertText,
|
||||
onRpcBefore,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { asyncStep, waitForSteps, Command, serverState } from "@web/../tests/web_test_helpers";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("auto layout with link preview list", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "test description",
|
||||
og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif",
|
||||
og_mimetype: "image/gif",
|
||||
og_title: "Yay Minions GIF - Yay Minions Happiness - Discover & Share GIFs",
|
||||
og_type: "video.other",
|
||||
source_url: "https://tenor.com/view/yay-minions-happiness-happy-excited-gif-15324023",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message .o-mail-LinkPreviewList");
|
||||
});
|
||||
|
||||
test("auto layout with link preview as gif", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "test description",
|
||||
og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif",
|
||||
og_mimetype: "image/gif",
|
||||
og_title: "Yay Minions GIF - Yay Minions Happiness - Discover & Share GIFs",
|
||||
og_type: "video.other",
|
||||
source_url: "https://tenor.com/view/yay-minions-happiness-happy-excited-gif-15324023",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-LinkPreviewImage");
|
||||
});
|
||||
|
||||
test("simplest card layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "Description",
|
||||
og_title: "Article title",
|
||||
og_type: "article",
|
||||
source_url: "https://www.odoo.com",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-LinkPreviewCard");
|
||||
await contains(".o-mail-LinkPreviewCard h6", { text: "Article title" });
|
||||
await contains(".o-mail-LinkPreviewCard p", { text: "Description" });
|
||||
});
|
||||
|
||||
test("simplest card layout with image", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "Description",
|
||||
og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif",
|
||||
og_title: "Article title",
|
||||
og_type: "article",
|
||||
source_url: "https://www.odoo.com",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-LinkPreviewCard");
|
||||
await contains(".o-mail-LinkPreviewCard h6", { text: "Article title" });
|
||||
await contains(".o-mail-LinkPreviewCard p", { text: "Description" });
|
||||
await contains(".o-mail-LinkPreviewCard img");
|
||||
});
|
||||
|
||||
test("Link preview video layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "Description",
|
||||
og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif",
|
||||
og_title: "video title",
|
||||
og_type: "video.other",
|
||||
source_url: "https://www.odoo.com",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-LinkPreviewVideo");
|
||||
await contains(".o-mail-LinkPreviewVideo h6", { text: "video title" });
|
||||
await contains(".o-mail-LinkPreviewVideo p", { text: "Description" });
|
||||
await contains(".o-mail-LinkPreviewVideo-overlay");
|
||||
});
|
||||
|
||||
test("Link preview image layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
image_mimetype: "image/jpg",
|
||||
source_url:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-LinkPreviewImage");
|
||||
});
|
||||
|
||||
test("Remove link preview Gif", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "test description",
|
||||
og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif",
|
||||
og_mimetype: "image/gif",
|
||||
og_title: "Yay Minions GIF - Yay Minions Happiness - Discover & Share GIFs",
|
||||
og_type: "video.other",
|
||||
source_url: "https://tenor.com/view/yay-minions-happiness-happy-excited-gif-15324023",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-LinkPreviewImage button[aria-label='Remove']");
|
||||
await contains("p", { text: "Do you really want to delete this preview?" });
|
||||
await click(".modal-footer button", { text: "Delete" });
|
||||
await contains(".o-mail-LinkPreviewImage", { count: 0 });
|
||||
});
|
||||
|
||||
test("Remove link preview card", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "Description",
|
||||
og_title: "Article title",
|
||||
og_type: "article",
|
||||
source_url: "https://www.odoo.com",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-LinkPreviewCard button[aria-label='Remove']");
|
||||
await contains("p", { text: "Do you really want to delete this preview?" });
|
||||
await click(".modal-footer button", { text: "Delete" });
|
||||
await contains(".o-mail-LinkPreviewCard", { count: 0 });
|
||||
});
|
||||
|
||||
test("Remove link preview video", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "Description",
|
||||
og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif",
|
||||
og_title: "video title",
|
||||
og_type: "video.other",
|
||||
source_url: "https://www.odoo.com",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-LinkPreviewVideo button[aria-label='Remove']");
|
||||
await contains("p", { text: "Do you really want to delete this preview?" });
|
||||
await click(".modal-footer button", { text: "Delete" });
|
||||
await contains(".o-mail-LinkPreviewVideo", { count: 0 });
|
||||
});
|
||||
|
||||
test("Remove link preview image", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
image_mimetype: "image/jpg",
|
||||
source_url:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-LinkPreviewImage button[aria-label='Remove']");
|
||||
await contains("p", { text: "Do you really want to delete this preview?" });
|
||||
await click(".modal-footer button", { text: "Delete" });
|
||||
await contains(".o-mail-LinkPreviewImage", { count: 0 });
|
||||
});
|
||||
|
||||
test("No crash on receiving link preview of non-known message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
image_mimetype: "image/jpg",
|
||||
source_url:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "https://make-link-preview.com",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
const messageLinkPreviewId = pyEnv["mail.message.link.preview"].create({
|
||||
message_id: messageId,
|
||||
link_preview_id: linkPreviewId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss();
|
||||
rpc("/mail/link_preview", { message_id: messageId });
|
||||
rpc("/mail/link_preview/hide", { message_link_preview_ids: [messageLinkPreviewId] });
|
||||
expect(true).toBe(true, { message: "no assertions" });
|
||||
});
|
||||
|
||||
test("Squash the message and the link preview when the link preview is an image and the link is the only text in the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
image_mimetype: "image/jpg",
|
||||
source_url:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<a href='linkPreviewLink'>http://linkPreview</a>",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-LinkPreviewImage");
|
||||
await contains(".o-mail-Message-bubble", { count: 0 });
|
||||
});
|
||||
|
||||
test("Link preview and message should not be squashed when the link preview is not an image", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
og_description: "Description",
|
||||
og_title: "Article title",
|
||||
og_type: "article",
|
||||
source_url: "https://www.odoo.com",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<a href='linkPreviewLink'>http://linkPreview</a>",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message-bubble");
|
||||
});
|
||||
|
||||
test("Link preview and message should not be squashed when there is more than the link in the message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv["mail.link.preview"].create({
|
||||
image_mimetype: "image/jpg",
|
||||
source_url:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "<a href='linkPreviewLink'>http://linkPreview</a> not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId })],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-Message-bubble");
|
||||
});
|
||||
|
||||
test("Sending message with link preview URL should show a link preview card", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "https://make-link-preview.com");
|
||||
await press("Enter");
|
||||
await contains(".o-mail-LinkPreviewCard");
|
||||
});
|
||||
|
||||
test("Delete all link previews at once", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [linkPreviewId_1, linkPreviewId_2] = pyEnv["mail.link.preview"].create([
|
||||
{
|
||||
og_description: "Description",
|
||||
og_title: "Article title 1",
|
||||
og_type: "article",
|
||||
source_url: "https://www.odoo.com",
|
||||
},
|
||||
{
|
||||
image_mimetype: "image/jpg",
|
||||
source_url:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg",
|
||||
},
|
||||
]);
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "wololo" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [
|
||||
Command.create({ link_preview_id: linkPreviewId_1 }),
|
||||
Command.create({ link_preview_id: linkPreviewId_2 }),
|
||||
],
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-LinkPreviewCard button[aria-label='Remove']");
|
||||
await click(".modal-footer button", { text: "Delete all previews" });
|
||||
await contains(".o-mail-LinkPreviewCard", { count: 0 });
|
||||
await contains(".o-mail-LinkPreviewImage", { count: 0 });
|
||||
});
|
||||
|
||||
test("link preview request is only made when message contains URL", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Sales" });
|
||||
onRpcBefore("/mail/link_preview", () => asyncStep("/mail/link_preview"));
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "Hello, this message does not contain any link");
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message", {
|
||||
text: "Hello, this message does not contain any link",
|
||||
});
|
||||
await waitForSteps([]);
|
||||
await insertText(".o-mail-Composer-input", "#");
|
||||
await click(".o-mail-NavigableList-item", { text: "Sales" });
|
||||
await press("Enter");
|
||||
await contains(".o-mail-Message", { text: "Sales" });
|
||||
await waitForSteps([]);
|
||||
await insertText(".o-mail-Composer-input", "https://www.odoo.com");
|
||||
await press("Enter");
|
||||
await waitForSteps(["/mail/link_preview"]);
|
||||
});
|
||||
|
||||
test("youtube and gdrive videos URL are embed", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const [linkPreviewId_1, linkPreviewId_2] = pyEnv["mail.link.preview"].create([
|
||||
{
|
||||
og_title: "vokoscreenNG-2024-08-22_13-56-37.mkv",
|
||||
og_type: "article",
|
||||
source_url: "https://drive.google.com/file/d/195a8fSNxwmkfs9sDS7OCB2nX03iFr21P/view",
|
||||
},
|
||||
{
|
||||
og_title: "Cinematic",
|
||||
og_type: "video",
|
||||
source_url: "https://www.youtube.com/watch?v=9bZkp7q19f0",
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "GDrive video preview",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId_1 })],
|
||||
},
|
||||
{
|
||||
body: "YT video preview",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId_2 })],
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-LinkPreviewVideo[data-provider=google-drive] .fa-play");
|
||||
await contains(
|
||||
"iframe[data-src='https://drive.google.com/file/d/195a8fSNxwmkfs9sDS7OCB2nX03iFr21P/preview']",
|
||||
{ parent: [".o-mail-LinkPreviewVideo[data-provider=google-drive]"] }
|
||||
);
|
||||
await click(".o-mail-LinkPreviewVideo[data-provider=youtube] .fa-play");
|
||||
await contains("iframe[data-src='https://www.youtube.com/embed/9bZkp7q19f0?autoplay=1']", {
|
||||
parent: [".o-mail-LinkPreviewVideo[data-provider=youtube]"],
|
||||
});
|
||||
});
|
||||
|
||||
test("Internal user can't delete others preview", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [linkPreviewId_1, linkPreviewId_2] = pyEnv["mail.link.preview"].create([
|
||||
{
|
||||
og_description: "Description",
|
||||
og_title: "Article title 1",
|
||||
og_type: "article",
|
||||
source_url: "https://www.odoo.com/",
|
||||
},
|
||||
{
|
||||
og_description: "Description",
|
||||
og_title: "Article title 2",
|
||||
og_type: "article",
|
||||
source_url: "https://example.com",
|
||||
},
|
||||
]);
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Test User" });
|
||||
const userId = pyEnv["res.users"].create({
|
||||
partner_id: partnerId,
|
||||
login: "testUser",
|
||||
password: "testUser",
|
||||
});
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "wololo",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
});
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "msg-1",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
author_id: serverState.partnerId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId_1 })],
|
||||
},
|
||||
{
|
||||
body: "msg-2",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
author_id: partnerId,
|
||||
message_link_preview_ids: [Command.create({ link_preview_id: linkPreviewId_2 })],
|
||||
},
|
||||
]);
|
||||
await start({ authenticateAs: pyEnv["res.users"].read(userId)[0] });
|
||||
await openDiscuss(channelId);
|
||||
await hover(".o-mail-Message:contains('msg-2') .o-mail-LinkPreviewCard");
|
||||
await contains(
|
||||
".o-mail-Message:contains('msg-2') .o-mail-LinkPreviewCard button[aria-label='Remove']"
|
||||
);
|
||||
await hover(".o-mail-Message:contains('msg-1') .o-mail-LinkPreviewCard");
|
||||
await contains(
|
||||
".o-mail-Message:contains('msg-1') .o-mail-LinkPreviewCard button[aria-label='Remove']",
|
||||
{ count: 0 }
|
||||
);
|
||||
});
|
||||
2330
odoo-bringout-oca-ocb-mail/mail/static/tests/message/message.test.js
Normal file
2330
odoo-bringout-oca-ocb-mail/mail/static/tests/message/message.test.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,269 @@
|
|||
import { insertText as htmlInsertText } from "@html_editor/../tests/_helpers/user_actions";
|
||||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
openDiscuss,
|
||||
start,
|
||||
startServer,
|
||||
openFormView,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { queryFirst } from "@odoo/hoot-dom";
|
||||
import { disableAnimations } from "@odoo/hoot-mock";
|
||||
import { getService, serverState } from "@web/../tests/web_test_helpers";
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("click on message in reply to highlight the parent message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "Hey lol",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Reply to Hey",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
parent_id: messageId,
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-MessageInReply-message", {
|
||||
parent: [".o-mail-Message", { text: "Reply to Hey" }],
|
||||
});
|
||||
await contains(".o-mail-Message.o-highlighted .o-mail-Message-content", { text: "Hey lol" });
|
||||
});
|
||||
|
||||
test("click on message in reply to scroll to the parent message", async () => {
|
||||
disableAnimations();
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const [oldestMessageId] = pyEnv["mail.message"].create(
|
||||
Array(20)
|
||||
.fill(0)
|
||||
.map(() => ({
|
||||
body: "Non Empty Body ".repeat(25),
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
}))
|
||||
);
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Response to first message",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
parent_id: oldestMessageId,
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-MessageInReply-message", {
|
||||
parent: [".o-mail-Message", { text: "Response to first message" }],
|
||||
});
|
||||
await contains(":nth-child(1 of .o-mail-Message)", { visible: true });
|
||||
});
|
||||
|
||||
test("reply shows correct author avatar", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "Hey there",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
const partner = pyEnv["res.partner"].search_read([["id", "=", serverState.partnerId]])[0];
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Howdy",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
author_id: partnerId,
|
||||
parent_id: messageId,
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(
|
||||
`.o-mail-MessageInReply-avatar[data-src='${`${getOrigin()}/web/image/res.partner/${
|
||||
serverState.partnerId
|
||||
}/avatar_128?unique=${deserializeDateTime(partner.write_date).ts}`}`
|
||||
);
|
||||
});
|
||||
|
||||
test("click on message in reply highlights original message", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Response to deleted message",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
parent_id: messageId,
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(
|
||||
".o-mail-Message:contains('Response to deleted message') .o-mail-MessageInReply:contains('Original message was deleted') .cursor-pointer"
|
||||
);
|
||||
await contains(".o-mail-Message.o-highlighted:contains('This message has been removed')");
|
||||
});
|
||||
|
||||
test("can reply to logged note in chatter", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerBId = pyEnv["res.partner"].create({ name: "Partner B" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
author_id: partnerBId,
|
||||
body: "Test message from B",
|
||||
model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
subtype_id: pyEnv["mail.message.subtype"].search([
|
||||
["subtype_xmlid", "=", "mail.mt_note"],
|
||||
])[0],
|
||||
},
|
||||
{
|
||||
author_id: serverState.partnerId,
|
||||
body: "Another msg",
|
||||
model: "discuss.channel",
|
||||
message_type: "comment",
|
||||
res_id: channelId,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-Message [title='Expand']");
|
||||
await contains(".o-dropdown-item:contains('Reply')");
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click(".o-mail-Message:contains('Test message from B') [title='Reply']");
|
||||
await contains("button.active", { text: "Log note" });
|
||||
await contains(".o-mail-Composer.o-focused .o-mail-Composer-input", { value: "@Partner B " });
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o-mail-Message a.o_mail_redirect", { text: "@Partner B" });
|
||||
await contains(".o-mail-Message:contains('@Partner B') [title='Edit']");
|
||||
await contains(".o-mail-Message:contains('@Partner B') [title='Reply']", { count: 0 });
|
||||
await click(".o-mail-Message:contains('@Partner B') [title='Expand']");
|
||||
await contains(".o-dropdown-item:contains('Delete')");
|
||||
await contains(".o-dropdown-item:contains('Reply')", { count: 0 });
|
||||
});
|
||||
|
||||
test.tags("html composer");
|
||||
test("reply to logged note in chatter keeps prefilled mention in html composer", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerBId = pyEnv["res.partner"].create({ name: "Partner B" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerBId,
|
||||
body: "Test message from B",
|
||||
model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
subtype_id: pyEnv["mail.message.subtype"].search([
|
||||
["subtype_xmlid", "=", "mail.mt_note"],
|
||||
])[0],
|
||||
});
|
||||
await start();
|
||||
getService("mail.composer").setHtmlComposer();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click(".o-mail-Message:contains('Test message from B') [title='Reply']");
|
||||
await contains("button.active:text('Log note')");
|
||||
await contains(".o-mail-Composer.o-focused .o-mail-Composer-html.odoo-editor-editable");
|
||||
await contains(".o-mail-Composer-html.odoo-editor-editable a.o_mail_redirect:text('@Partner B')");
|
||||
const editor = {
|
||||
document,
|
||||
editable: queryFirst(".o-mail-Composer-html.odoo-editor-editable"),
|
||||
};
|
||||
await htmlInsertText(editor, "Hello");
|
||||
await contains(".o-mail-Composer-send:enabled");
|
||||
await click(".o-mail-Composer-send:enabled");
|
||||
await contains(".o-mail-Message:contains('Hello') a.o_mail_redirect:text('@Partner B')");
|
||||
});
|
||||
|
||||
test("Replying to a message containing line breaks should be correctly inlined", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "<p>Message first line.<br>Message second line.<br>Message third line.</p>",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
|
||||
pyEnv["mail.message"].create({
|
||||
body: "Howdy",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
author_id: partnerId,
|
||||
parent_id: messageId,
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageInReply-message", {
|
||||
text: "Message first line. Message second line. Message third line.",
|
||||
});
|
||||
});
|
||||
|
||||
test("reply with only attachment shows parent message context", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
|
||||
const originalMessageId = pyEnv["mail.message"].create({
|
||||
body: "Original message content",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const attachmentId = pyEnv["ir.attachment"].create({
|
||||
name: "test_image.png",
|
||||
mimetype: "image/png",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
attachment_ids: [attachmentId],
|
||||
body: "",
|
||||
message_type: "comment",
|
||||
model: "discuss.channel",
|
||||
parent_id: originalMessageId,
|
||||
res_id: channelId,
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await contains(".o-mail-MessageInReply-message", {
|
||||
text: "Original message content",
|
||||
});
|
||||
});
|
||||
|
||||
test("replying to a note restores focus on an already open composer", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerBId = pyEnv["res.partner"].create({ name: "Partner B" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerBId,
|
||||
body: "Test message from B",
|
||||
model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
subtype_id: pyEnv["mail.message.subtype"].search([
|
||||
["subtype_xmlid", "=", "mail.mt_note"],
|
||||
])[0],
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", serverState.partnerId);
|
||||
await click("button:not(.active):text('Log note')");
|
||||
await contains(".o-mail-Composer.o-focused");
|
||||
queryFirst(".o-mail-Composer-input").blur();
|
||||
await contains(".o-mail-Composer.o-focused", { count: 0 });
|
||||
await click(".o-mail-Message-actions [title='Reply']");
|
||||
await contains(".o-mail-Composer.o-focused");
|
||||
});
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
listenStoreFetch,
|
||||
openDiscuss,
|
||||
openFormView,
|
||||
start,
|
||||
startServer,
|
||||
waitStoreFetch,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
|
||||
import { Command, getService, serverState, withUser } from "@web/../tests/web_test_helpers";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("Receiving a new message out of discuss app should open a chat bubble", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Dumbledore" });
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
listenStoreFetch("init_messaging");
|
||||
await start();
|
||||
await waitStoreFetch("init_messaging");
|
||||
// send after init_messaging because bus subscription is done after init_messaging
|
||||
// simulate receving new message
|
||||
withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "Magic!", message_type: "comment" },
|
||||
thread_id: channelId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(".o-mail-ChatBubble[name='Dumbledore']");
|
||||
});
|
||||
|
||||
test("Show conversations with new message in chat hub (outside of discuss app)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Dumbledore" });
|
||||
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const [chatId, groupChatId] = pyEnv["discuss.channel"].create([
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
},
|
||||
{
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "group",
|
||||
name: "GroupChat",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
getService("bus_service").subscribe("discuss.channel/new_message", () =>
|
||||
expect.step("discuss.channel/new_message")
|
||||
);
|
||||
// simulate receiving new message (chat, outside discuss app)
|
||||
await withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "Chat Message 1", message_type: "comment" },
|
||||
thread_id: chatId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await expect.waitForSteps(["discuss.channel/new_message"]);
|
||||
await contains(".o-mail-ChatBubble .badge:contains(1)", { count: 1 });
|
||||
await click(".o-mail-ChatBubble[name='Dumbledore']");
|
||||
await contains(".o-mail-ChatWindow-header:contains('Dumbledore')");
|
||||
await contains(".o-mail-Message:contains('Chat Message 1')");
|
||||
await contains(".badge", { count: 0 });
|
||||
await click(".o-mail-ChatWindow [title*='Close Chat Window']");
|
||||
// simulate receiving new message (group chat, outside discuss app)
|
||||
await withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "GroupChat Message", message_type: "comment" },
|
||||
thread_id: groupChatId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await expect.waitForSteps(["discuss.channel/new_message"]);
|
||||
await contains(".o-mail-ChatBubble[name='GroupChat']");
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-Discuss[data-active]");
|
||||
// simulate receiving new message (chat, inside discuss app)
|
||||
await contains(".o-mail-DiscussSidebar-item:contains('Dumbledore') .badge", { count: 0 });
|
||||
await withUser(userId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: { body: "Tricky", message_type: "comment" },
|
||||
thread_id: chatId,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await expect.waitForSteps(["discuss.channel/new_message"]);
|
||||
await click(".o-mail-DiscussSidebar-item:contains('Dumbledore'):has(.badge:contains(1))");
|
||||
await contains(".o-mail-Message:contains('Tricky')");
|
||||
// check no new chat window/bubble while in discuss app
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-ChatBubble[name='GroupChat']");
|
||||
await contains(".o-mail-ChatBubble[name='Dumbledore']", { count: 0 });
|
||||
await contains(".o-mail-ChatWindow-header:contains('Dumbledore')", { count: 0 });
|
||||
});
|
||||
|
||||
test("Posting a message in discuss app should not open a chat window after leaving discuss app", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Dumbledore" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "test https://www.odoo.com/");
|
||||
await press("Enter");
|
||||
// leaving discuss.
|
||||
await openFormView("res.partner", partnerId);
|
||||
// weak test, no guarantee that we waited long enough for the potential chat window to open
|
||||
await contains(".o-mail-ChatWindow", { count: 0, text: "Dumbledore" });
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,374 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
start,
|
||||
startServer,
|
||||
triggerEvents,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { rpcWithEnv } from "@mail/utils/common/misc";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
import {
|
||||
asyncStep,
|
||||
Command,
|
||||
mockService,
|
||||
serverState,
|
||||
waitForSteps,
|
||||
withUser,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
/** @type {ReturnType<import("@mail/utils/common/misc").rpcWithEnv>} */
|
||||
let rpc;
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMailModels();
|
||||
|
||||
test("basic layout", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
date: "2019-01-01 10:30:00",
|
||||
message_type: "email",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem", {
|
||||
contains: [
|
||||
[".o-mail-NotificationItem-name", { text: "Email Failure: Discussion Channel" }],
|
||||
[".o-mail-NotificationItem-counter", { text: "2" }],
|
||||
[".o-mail-NotificationItem-date", { text: "Jan 1" }],
|
||||
[".o-mail-NotificationItem-text", { text: "An error occurred when sending an email" }],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("mark as read", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
message_type: "email",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await triggerEvents(".o-mail-NotificationItem", ["mouseenter"], {
|
||||
text: "Email Failure: Discussion Channel",
|
||||
});
|
||||
await click(".o-mail-NotificationItem-markAsRead", {
|
||||
parent: [".o-mail-NotificationItem", { text: "Email Failure: Discussion Channel" }],
|
||||
});
|
||||
await contains(".o-mail-NotificationItem", {
|
||||
count: 0,
|
||||
text: "Email Failure: Discussion Channel",
|
||||
});
|
||||
});
|
||||
|
||||
test("open non-channel failure", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([{}, {}]);
|
||||
const [messageId_1, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
message_type: "email",
|
||||
model: "res.partner",
|
||||
res_id: partnerId_1,
|
||||
},
|
||||
{
|
||||
message_type: "email",
|
||||
model: "res.partner",
|
||||
res_id: partnerId_2,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "bounce",
|
||||
notification_type: "email",
|
||||
},
|
||||
]);
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
asyncStep("do_action");
|
||||
expect(action.name).toBe("Mail Failures");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.view_mode).toBe("kanban,list,form");
|
||||
expect(JSON.stringify(action.views)).toBe(
|
||||
JSON.stringify([
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
])
|
||||
);
|
||||
expect(action.target).toBe("current");
|
||||
expect(action.res_model).toBe("res.partner");
|
||||
expect(JSON.stringify(action.domain)).toBe(
|
||||
JSON.stringify([["message_has_error", "=", true]])
|
||||
);
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem");
|
||||
await waitForSteps(["do_action"]);
|
||||
});
|
||||
|
||||
test("different discuss.channel are not grouped", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [channelId_1, channelId_2] = pyEnv["discuss.channel"].create([
|
||||
{ name: "Channel_1" },
|
||||
{ name: "Channel_2" },
|
||||
]);
|
||||
const [messageId_1, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
message_type: "email",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId_1,
|
||||
},
|
||||
{
|
||||
message_type: "email",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId_2,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "bounce",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "bounce",
|
||||
notification_type: "email",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem-text", {
|
||||
count: 2,
|
||||
text: "An error occurred when sending an email",
|
||||
});
|
||||
});
|
||||
|
||||
test("multiple grouped notifications by model", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const companyId = pyEnv["res.company"].create({});
|
||||
const [messageId_1, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
message_type: "email",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
},
|
||||
{
|
||||
message_type: "email",
|
||||
model: "res.company",
|
||||
res_id: companyId,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "bounce",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "bounce",
|
||||
notification_type: "email",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem", { count: 2 });
|
||||
await contains(".o-mail-NotificationItem-counter", { count: 2, text: "2" });
|
||||
});
|
||||
|
||||
test("non-failure notifications are ignored", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
message_type: "email",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "ready",
|
||||
notification_type: "email",
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem", { count: 0 });
|
||||
});
|
||||
|
||||
test("marked as read thread notifications are ordered by last message date", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [channelId_1, channelId_2] = pyEnv["discuss.channel"].create([
|
||||
{ name: "Channel 2019" },
|
||||
{ name: "Channel 2020" },
|
||||
]);
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
body: "not empty",
|
||||
date: "2019-01-01 00:00:00",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId_1,
|
||||
},
|
||||
{
|
||||
body: "not empty",
|
||||
date: "2020-01-01 00:00:00",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId_2,
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem", { count: 2 });
|
||||
await contains(":nth-child(1 of .o-mail-NotificationItem)", { text: "Channel 2020" });
|
||||
await contains(":nth-child(2 of .o-mail-NotificationItem)", { text: "Channel 2019" });
|
||||
});
|
||||
|
||||
test("thread notifications are re-ordered on receiving a new message", async () => {
|
||||
mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!)
|
||||
const pyEnv = await startServer();
|
||||
const bobUserId = pyEnv["res.users"].create({ name: "Bob" });
|
||||
const bobPartnerId = pyEnv["res.partner"].create({ name: "Bob", user_id: bobUserId.id });
|
||||
const [channelId_1, channelId_2] = pyEnv["discuss.channel"].create([
|
||||
{
|
||||
name: "Channel 2019",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: bobPartnerId }),
|
||||
Command.create({ partner_id: serverState.partnerId }),
|
||||
],
|
||||
},
|
||||
{ name: "Channel 2020" },
|
||||
]);
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
date: "2019-01-01 00:00:00",
|
||||
body: "some text",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId_1,
|
||||
},
|
||||
{
|
||||
date: "2020-01-01 00:00:00",
|
||||
body: "some text 2",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId_2,
|
||||
},
|
||||
]);
|
||||
const env = await start();
|
||||
rpc = rpcWithEnv(env);
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem", { count: 2 });
|
||||
await withUser(bobUserId, () =>
|
||||
rpc("/mail/message/post", {
|
||||
post_data: {
|
||||
body: "<p>New message !</p>",
|
||||
message_type: "comment",
|
||||
subtype_xmlid: "mail.mt_comment",
|
||||
},
|
||||
thread_id: channelId_1,
|
||||
thread_model: "discuss.channel",
|
||||
})
|
||||
);
|
||||
await contains(":nth-child(1 of .o-mail-NotificationItem)", { text: "Channel 2019" });
|
||||
await contains(":nth-child(2 of .o-mail-NotificationItem)", { text: "Channel 2020" });
|
||||
await contains(".o-mail-NotificationItem", { count: 2 });
|
||||
});
|
||||
|
||||
test("messaging menu counter should ignore unread messages in channels that are unpinned", async () => {
|
||||
mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!)
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
pyEnv["discuss.channel"].create({ name: "General" });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({
|
||||
unpin_dt: "2023-01-01 12:00:00",
|
||||
last_interest_dt: "2023-01-01 11:00:00",
|
||||
message_unread_counter: 1,
|
||||
partner_id: serverState.partnerId,
|
||||
}),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["mail.message"].create([
|
||||
{
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
author_id: partnerId,
|
||||
message_type: "email",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await contains(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-MessagingMenu-counter", { count: 0 });
|
||||
await click(".o_menu_systray i[aria-label='Messages']"); // fetch channels
|
||||
await contains(".o-mail-NotificationItem", { text: "General" }); // ensure channels fetched
|
||||
await contains(".o-mail-MessagingMenu-counter", { count: 0 });
|
||||
});
|
||||
|
||||
test("subtype description should be displayed when body is empty", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Partner1" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "Test" });
|
||||
const subtypeId = pyEnv["mail.message.subtype"].create({ description: "hello" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
subtype_id: subtypeId,
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem-text", { text: "Partner1: hello" });
|
||||
});
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import {
|
||||
mailCanAddMessageReactionMobile,
|
||||
mailCanCopyTextToClipboardMobile,
|
||||
} from "@mail/../tests/mail_shared_tests";
|
||||
import {
|
||||
SIZES,
|
||||
assertChatHub,
|
||||
click,
|
||||
contains,
|
||||
defineMailModels,
|
||||
insertText,
|
||||
listenStoreFetch,
|
||||
openDiscuss,
|
||||
openFormView,
|
||||
openListView,
|
||||
patchUiSize,
|
||||
setupChatHub,
|
||||
start,
|
||||
startServer,
|
||||
waitStoreFetch,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { LONG_PRESS_DELAY } from "@mail/utils/common/hooks";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { advanceTime, pointerDown, press } from "@odoo/hoot-dom";
|
||||
import { Deferred, mockTouch, mockUserAgent } from "@odoo/hoot-mock";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { asyncStep, serverState, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("mobile");
|
||||
defineMailModels();
|
||||
|
||||
test("auto-select 'Inbox' when discuss had channel as active thread", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await click(".o-mail-ChatWindow [title*='Close Chat Window']");
|
||||
await contains(".o-mail-MessagingMenu-tab.active", { text: "Channels" });
|
||||
await click("button", { text: "Inbox" });
|
||||
await contains(".o-mail-MessagingMenu-tab.active", { text: "Inbox" });
|
||||
await contains(".btn-secondary.active", { text: "Inbox" }); // in header
|
||||
});
|
||||
|
||||
test("show loading on initial opening", async () => {
|
||||
// This could load a lot of data (all pinned conversations)
|
||||
const def = new Deferred();
|
||||
listenStoreFetch("channels_as_member", {
|
||||
async onRpc() {
|
||||
asyncStep("before channels_as_member");
|
||||
await def;
|
||||
},
|
||||
});
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["discuss.channel"].create({ name: "General" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-MessagingMenu .fa.fa-circle-o-notch.fa-spin");
|
||||
await contains(".o-mail-NotificationItem", { text: "General", count: 0 });
|
||||
await waitForSteps(["before channels_as_member"]);
|
||||
def.resolve();
|
||||
await waitStoreFetch("channels_as_member");
|
||||
await contains(".o-mail-MessagingMenu .fa.fa-circle-o-notch.fa-spin", { count: 0 });
|
||||
await contains(".o-mail-NotificationItem", { text: "General" });
|
||||
});
|
||||
|
||||
test("can leave channel in mobile", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
// dropdown requires an extra delay before click (because handler is registered in useEffect)
|
||||
await contains(".o-mail-ChatWindow-moreActions", { text: "General" });
|
||||
await click(".o-mail-ChatWindow-moreActions", { text: "General" });
|
||||
await contains(".o-dropdown-item", { text: "Leave Channel" });
|
||||
});
|
||||
|
||||
test("enter key should create a newline in composer", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", "Test\n");
|
||||
await press("Enter");
|
||||
await insertText(".o-mail-Composer-input", "Other");
|
||||
await click(".fa-paper-plane-o");
|
||||
await contains(".o-mail-Message-body:has(br)", { textContent: "TestOther" });
|
||||
});
|
||||
|
||||
test("can add message reaction (mobile)", mailCanAddMessageReactionMobile);
|
||||
|
||||
test("can copy text to clipboard (mobile)", mailCanCopyTextToClipboardMobile);
|
||||
|
||||
test("Can edit message comment in chatter (mobile)", async () => {
|
||||
mockTouch(true);
|
||||
mockUserAgent("android");
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "TestPartner" });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: serverState.partnerId,
|
||||
body: "original message",
|
||||
message_type: "comment",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message", { text: "original message" });
|
||||
await pointerDown(".o-mail-Message", { contains: "original message" });
|
||||
await advanceTime(LONG_PRESS_DELAY);
|
||||
await click("button", { text: "Edit" });
|
||||
await click("button", { text: "Discard editing" });
|
||||
await contains(".o-mail-Message", { text: "original message" });
|
||||
await pointerDown(".o-mail-Message", { contains: "original message" });
|
||||
await advanceTime(LONG_PRESS_DELAY);
|
||||
await click("button", { text: "Edit" });
|
||||
await insertText(".o-mail-Message .o-mail-Composer-input", "edited message", { replace: true });
|
||||
await click("button[title='Save editing']");
|
||||
await contains(".o-mail-Message", { text: "edited message (edited)" });
|
||||
});
|
||||
|
||||
test("Don't show chat hub in discuss app on mobile", async () => {
|
||||
mockTouch(true);
|
||||
mockUserAgent("android");
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
|
||||
setupChatHub({ folded: [channelId] });
|
||||
await start();
|
||||
await contains(".o-mail-ChatBubble");
|
||||
await openDiscuss();
|
||||
await contains(".o-mail-ChatBubble", { count: 0 });
|
||||
});
|
||||
|
||||
test("click on an odoo link should fold the chat window (mobile)", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const channelId = pyEnv["discuss.channel"].create({});
|
||||
patchUiSize({ size: SIZES.SM });
|
||||
await start();
|
||||
await openDiscuss(channelId);
|
||||
await insertText(".o-mail-Composer-input", `http://${browser.location.host}/odoo.com`);
|
||||
await click(".o-mail-Composer button[title='Send']");
|
||||
await contains(".o-mail-ChatWindow");
|
||||
await click(`a[href="http://${browser.location.host}/odoo.com"]`);
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
await contains(".o-mail-ChatBubble", { count: 0 });
|
||||
await openListView("discuss.channel", { res_id: channelId });
|
||||
await contains(".o-mail-ChatBubble");
|
||||
assertChatHub({ folded: [channelId] });
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
91
odoo-bringout-oca-ocb-mail/mail/static/tests/mock_server/mock_models/@types/mock_models.d.ts
vendored
Normal file
91
odoo-bringout-oca-ocb-mail/mail/static/tests/mock_server/mock_models/@types/mock_models.d.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
declare module "mock_models" {
|
||||
import { Base as Base2 } from "@mail/../tests/mock_server/mock_models/base";
|
||||
import { DiscussChannel as DiscussChannel2 } from "@mail/../tests/mock_server/mock_models/discuss_channel";
|
||||
import { DiscussChannelMember as DiscussChannelMember2 } from "@mail/../tests/mock_server/mock_models/discuss_channel_member";
|
||||
import { DiscussChannelRtcSession as DiscussChannelRtcSession2 } from "@mail/../tests/mock_server/mock_models/discuss_channel_rtc_session";
|
||||
import { DiscussVoiceMetadata as DiscussVoiceMetadata2 } from "@mail/../tests/mock_server/mock_models/discuss_voice_metadata";
|
||||
import { IrAttachment as IrAttachment2 } from "@mail/../tests/mock_server/mock_models/ir_attachment";
|
||||
import { MailActivity as MailActivity2 } from "@mail/../tests/mock_server/mock_models/mail_activity";
|
||||
import { MailActivityType as MailActivityType2 } from "@mail/../tests/mock_server/mock_models/mail_activity_type";
|
||||
import { MailFollowers as MailFollowers2 } from "@mail/../tests/mock_server/mock_models/mail_followers";
|
||||
import { MailGuest as MailGuest2 } from "@mail/../tests/mock_server/mock_models/mail_guest";
|
||||
import { MailLinkPreview as MailLinkPreview2 } from "@mail/../tests/mock_server/mock_models/mail_link_preview";
|
||||
import { MailMessage as MailMessage2 } from "@mail/../tests/mock_server/mock_models/mail_message";
|
||||
import { MailMessageLinkPreview as MailMessageLinkPreview2 } from "@mail/../tests/mock_server/mock_models/mail_message_link_preview";
|
||||
import { MailMessageReaction as MailMessageReaction2 } from "@mail/../tests/mock_server/mock_models/mail_message_reaction";
|
||||
import { MailMessageSubtype as MailMessageSubtype2 } from "@mail/../tests/mock_server/mock_models/mail_message_subtype";
|
||||
import { MailNotification as MailNotification2 } from "@mail/../tests/mock_server/mock_models/mail_notification";
|
||||
import { MailScheduledMessage as MailScheduledMessage2 } from "@mail/.../tests/mock_server/mock_models/mail_scheduled_message";
|
||||
import { MailShortcode as MailShortcode2 } from "@mail/../tests/mock_server/mock_models/mail_shortcode";
|
||||
import { MailTemplate as MailTemplate2 } from "@mail/../tests/mock_server/mock_models/mail_template";
|
||||
import { MailThread as MailThread2 } from "@mail/../tests/mock_server/mock_models/mail_thread";
|
||||
import { MailTrackingValue as MailTrackingValue2 } from "@mail/../tests/mock_server/mock_models/mail_tracking_value";
|
||||
import { ResFake as ResFake2 } from "@mail/../tests/mock_server/mock_models/res_fake";
|
||||
import { ResLang as ResLang2 } from "@mail/../tests/mock_server/mock_models/res_lang";
|
||||
import { ResRole as ResRole2 } from "addons/mail/static/tests/mock_server/mock_models/res_role";
|
||||
import { ResPartner as ResPartner2 } from "@mail/../tests/mock_server/mock_models/res_partner";
|
||||
import { ResUsers as ResUsers2 } from "@mail/../tests/mock_server/mock_models/res_users";
|
||||
import { ResUsersSettings as ResUsersSettings2 } from "@mail/../tests/mock_server/mock_models/res_users_settings";
|
||||
import { ResUsersSettingsVolumes as ResUsersSettingsVolumes2 } from "@mail/../tests/mock_server/mock_models/res_users_settings_volumes";
|
||||
|
||||
export interface Base extends Base2 {}
|
||||
export interface DiscussChannel extends DiscussChannel2 {}
|
||||
export interface DiscussChannelMember extends DiscussChannelMember2 {}
|
||||
export interface DiscussChannelRtcSession extends DiscussChannelRtcSession2 {}
|
||||
export interface DiscussVoiceMetadata extends DiscussVoiceMetadata2 {}
|
||||
export interface IrAttachment extends IrAttachment2 {}
|
||||
export interface MailActivity extends MailActivity2 {}
|
||||
export interface MailActivityType extends MailActivityType2 {}
|
||||
export interface MailFollowers extends MailFollowers2 {}
|
||||
export interface MailGuest extends MailGuest2 {}
|
||||
export interface MailLinkPreview extends MailLinkPreview2 {}
|
||||
export interface MailMessage extends MailMessage2 {}
|
||||
export interface MailMessageLinkPreview extends MailMessageLinkPreview2 {}
|
||||
export interface MailMessageReaction extends MailMessageReaction2 {}
|
||||
export interface MailMessageSubtype extends MailMessageSubtype2 {}
|
||||
export interface MailNotification extends MailNotification2 {}
|
||||
export interface MailScheduledMessage extends MailScheduledMessage2 {}
|
||||
export interface MailShortcode extends MailShortcode2 {}
|
||||
export interface MailTemplate extends MailTemplate2 {}
|
||||
export interface MailThread extends MailThread2 {}
|
||||
export interface MailTrackingValue extends MailTrackingValue2 {}
|
||||
export interface ResFake extends ResFake2 {}
|
||||
export interface ResLang extends ResLang2 {}
|
||||
export interface ResPartner extends ResPartner2 {}
|
||||
export interface ResRole extends ResRole2 {}
|
||||
export interface ResUsers extends ResUsers2 {}
|
||||
export interface ResUsersSettings extends ResUsersSettings2 {}
|
||||
export interface ResUsersSettingsVolumes extends ResUsersSettingsVolumes2 {}
|
||||
|
||||
export interface Models {
|
||||
"base": Base,
|
||||
"discuss.channel": DiscussChannel,
|
||||
"discuss.channel.member": DiscussChannelMember,
|
||||
"discuss.channel.rtc.session": DiscussChannelRtcSession,
|
||||
"discuss.voice.metadata": DiscussVoiceMetadata,
|
||||
"ir.attachment": IrAttachment,
|
||||
"mail.activity": MailActivity,
|
||||
"mail.activity.type": MailActivityType,
|
||||
"mail.followers": MailFollowers,
|
||||
"mail.guest": MailGuest,
|
||||
"mail.link.preview": MailLinkPreview,
|
||||
"mail.message": MailMessage,
|
||||
"mail.message.link.preview": MailMessageLinkPreview,
|
||||
"mail.message.reaction": MailMessageReaction,
|
||||
"mail.message.subtype": MailMessageSubtype,
|
||||
"mail.notification": MailNotification,
|
||||
"mail.scheduled.message": MailScheduledMessage,
|
||||
"mail.shortcode": MailShortcode,
|
||||
"mail.template": MailTemplate,
|
||||
"mail.thread": MailThread,
|
||||
"mail.tracking.value": MailTrackingValue,
|
||||
"res.fake": ResFake,
|
||||
"res.groups": ResGroups,
|
||||
"res.lang": ResLang,
|
||||
"res.partner": ResPartner,
|
||||
"res.role": ResRole,
|
||||
"res.users": ResUsers,
|
||||
"res.users.settings": ResUsersSettings,
|
||||
"res.users.settings.volumes": ResUsersSettingsVolumes,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { getKwArgs, models } from "@web/../tests/web_test_helpers";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(models.ServerModel.prototype, {
|
||||
/**
|
||||
* @override
|
||||
* @type {typeof models.ServerModel["prototype"]["get_views"]}
|
||||
*/
|
||||
get_views() {
|
||||
const result = super.get_views(...arguments);
|
||||
for (const modelName of Object.keys(result.models)) {
|
||||
if (this.has_activities) {
|
||||
result.models[modelName].has_activities = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export class Base extends models.ServerModel {
|
||||
_name = "base";
|
||||
|
||||
/**
|
||||
* @param {Object} trackedFieldNamesToField
|
||||
* @param {Object} initialTrackedFieldValues
|
||||
* @param {Object} record
|
||||
*/
|
||||
_mail_track(trackedFieldNamesToField, initialTrackedFieldValues, record) {
|
||||
const kwargs = getKwArgs(
|
||||
arguments,
|
||||
"trackedFieldNamesToField",
|
||||
"initialTrackedFieldValues",
|
||||
"record"
|
||||
);
|
||||
trackedFieldNamesToField = kwargs.trackedFieldNamesToField;
|
||||
initialTrackedFieldValues = kwargs.initialTrackedFieldValues;
|
||||
record = kwargs.record;
|
||||
|
||||
/** @type {import("mock_models").MailTrackingValue} */
|
||||
const MailTrackingValue = this.env["mail.tracking.value"];
|
||||
|
||||
const trackingValueIds = [];
|
||||
const changedFieldNames = [];
|
||||
for (const fname in trackedFieldNamesToField) {
|
||||
const initialValue = initialTrackedFieldValues[fname];
|
||||
const newValue = record[fname];
|
||||
if (!initialValue && !newValue) {
|
||||
continue;
|
||||
}
|
||||
if (initialValue !== newValue) {
|
||||
const tracking = MailTrackingValue._create_tracking_values(
|
||||
initialValue,
|
||||
newValue,
|
||||
fname,
|
||||
trackedFieldNamesToField[fname],
|
||||
this
|
||||
);
|
||||
if (tracking) {
|
||||
trackingValueIds.push(tracking);
|
||||
}
|
||||
changedFieldNames.push(fname);
|
||||
}
|
||||
}
|
||||
return { changedFieldNames, trackingValueIds };
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue