diff --git a/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js b/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js
index a27c07dc364..a02185918b9 100644
--- a/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js
+++ b/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js
@@ -1,9 +1,9 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
-import { insert } from '@mail/model';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
+import { insert } from "@mail/model";
-import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
+import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -18,7 +18,6 @@ const getNextId = (function () {
* records are ready before rendering ActivityCellView component.
*/
export class ActivityCellViewContainer extends Component {
-
/**
* @override
*/
@@ -28,7 +27,7 @@ export class ActivityCellViewContainer extends Component {
this.activityCellView = undefined;
this.activityCellViewId = getNextId();
this._insertFromProps(this.props);
- onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
+ onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -53,7 +52,7 @@ export class ActivityCellViewContainer extends Component {
this._deleteRecord();
return;
}
- const activityCellView = messaging.models['ActivityCellView'].insert({
+ const activityCellView = messaging.models["ActivityCellView"].insert({
activityType: {
id: props.activityTypeId,
},
@@ -61,14 +60,16 @@ export class ActivityCellViewContainer extends Component {
id: this.activityCellViewId,
reloadFunc: props.reloadFunc,
thread: {
- activities: insert(props.activityIds.map(activityId => {
- return {
- id: activityId,
- type: {
- id: props.activityTypeId,
- },
- };
- })),
+ activities: insert(
+ props.activityIds.map((activityId) => {
+ return {
+ id: activityId,
+ type: {
+ id: props.activityTypeId,
+ },
+ };
+ })
+ ),
hasActivities: true,
id: props.resId,
model: props.resModel,
@@ -80,7 +81,6 @@ export class ActivityCellViewContainer extends Component {
}
this.render();
}
-
}
Object.assign(ActivityCellViewContainer, {
@@ -95,5 +95,5 @@ Object.assign(ActivityCellViewContainer, {
resId: Number,
resModel: String,
},
- template: 'mail.ActivityCellViewContainer',
+ template: "mail.ActivityCellViewContainer",
});
diff --git a/addons/mail/static/src/backend_components/activity_exception/activity_exception.js b/addons/mail/static/src/backend_components/activity_exception/activity_exception.js
index 6df5883cacc..3778b0e3c8f 100644
--- a/addons/mail/static/src/backend_components/activity_exception/activity_exception.js
+++ b/addons/mail/static/src/backend_components/activity_exception/activity_exception.js
@@ -3,26 +3,26 @@
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
class ActivityException extends Component {
-
get textClass() {
if (this.props.value) {
- return 'text-' + this.props.value + ' fa ' + this.props.record.data.activity_exception_icon;
+ return (
+ "text-" + this.props.value + " fa " + this.props.record.data.activity_exception_icon
+ );
}
return undefined;
}
-
}
Object.assign(ActivityException, {
props: standardFieldProps,
- template: 'mail.ActivityException',
+ template: "mail.ActivityException",
fieldDependencies: {
- activity_exception_icon: { type: 'char' },
+ activity_exception_icon: { type: "char" },
},
noLabel: true,
});
-registry.category('fields').add('activity_exception', ActivityException);
+registry.category("fields").add("activity_exception", ActivityException);
diff --git a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js
index c88894382c0..1abcdb67139 100644
--- a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js
+++ b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js
@@ -1,12 +1,12 @@
/** @odoo-module **/
// ensure components are registered beforehand.
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
-import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
+import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -21,7 +21,6 @@ const getNextId = (function () {
* records are ready before rendering KanbanFieldActivityView component.
*/
export class KanbanFieldActivityViewContainer extends Component {
-
/**
* @override
*/
@@ -31,7 +30,7 @@ export class KanbanFieldActivityViewContainer extends Component {
this.kanbanFieldActivityView = undefined;
this.kanbanFieldActivityViewId = getNextId();
this._insertFromProps(this.props);
- onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
+ onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -56,10 +55,10 @@ export class KanbanFieldActivityViewContainer extends Component {
this._deleteRecord();
return;
}
- const kanbanFieldActivityView = messaging.models['KanbanFieldActivityView'].insert({
+ const kanbanFieldActivityView = messaging.models["KanbanFieldActivityView"].insert({
id: this.kanbanFieldActivityViewId,
thread: {
- activities: props.value.records.map(activityData => {
+ activities: props.value.records.map((activityData) => {
return {
id: activityData.resId,
};
@@ -76,22 +75,21 @@ export class KanbanFieldActivityViewContainer extends Component {
}
this.render();
}
-
}
Object.assign(KanbanFieldActivityViewContainer, {
fieldDependencies: {
- activity_exception_decoration: { type: 'selection' },
- activity_exception_icon: { type: 'char' },
- activity_state: { type: 'selection' },
- activity_summary: { type: 'char' },
- activity_type_icon: { type: 'char' },
- activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
+ activity_exception_decoration: { type: "selection" },
+ activity_exception_icon: { type: "char" },
+ activity_state: { type: "selection" },
+ activity_summary: { type: "char" },
+ activity_type_icon: { type: "char" },
+ activity_type_id: { type: "many2one", relation: "mail.activity.type" },
},
props: {
...standardFieldProps,
},
- template: 'mail.KanbanFieldActivityViewContainer',
+ template: "mail.KanbanFieldActivityViewContainer",
});
-registry.category('fields').add('kanban_activity', KanbanFieldActivityViewContainer);
+registry.category("fields").add("kanban_activity", KanbanFieldActivityViewContainer);
diff --git a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js
index bb6c3200085..7e60dd9eed4 100644
--- a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js
+++ b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js
@@ -1,12 +1,12 @@
/** @odoo-module **/
// ensure components are registered beforehand.
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
-import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
+import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -21,7 +21,6 @@ const getNextId = (function () {
* records are ready before rendering ListFieldActivityView component.
*/
export class ListFieldActivityViewContainer extends Component {
-
/**
* @override
*/
@@ -31,7 +30,7 @@ export class ListFieldActivityViewContainer extends Component {
this.listFieldActivityView = undefined;
this.listFieldActivityViewId = getNextId();
this._insertFromProps(this.props);
- onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
+ onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -56,10 +55,10 @@ export class ListFieldActivityViewContainer extends Component {
this._deleteRecord();
return;
}
- const listFieldActivityView = messaging.models['ListFieldActivityView'].insert({
+ const listFieldActivityView = messaging.models["ListFieldActivityView"].insert({
id: this.listFieldActivityViewId,
thread: {
- activities: props.value.records.map(activityData => {
+ activities: props.value.records.map((activityData) => {
return {
id: activityData.resId,
};
@@ -76,22 +75,21 @@ export class ListFieldActivityViewContainer extends Component {
}
this.render();
}
-
}
Object.assign(ListFieldActivityViewContainer, {
fieldDependencies: {
- activity_exception_decoration: { type: 'selection' },
- activity_exception_icon: { type: 'char' },
- activity_state: { type: 'selection' },
- activity_summary: { type: 'char' },
- activity_type_icon: { type: 'char' },
- activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
+ activity_exception_decoration: { type: "selection" },
+ activity_exception_icon: { type: "char" },
+ activity_state: { type: "selection" },
+ activity_summary: { type: "char" },
+ activity_type_icon: { type: "char" },
+ activity_type_id: { type: "many2one", relation: "mail.activity.type" },
},
props: {
...standardFieldProps,
},
- template: 'mail.ListFieldActivityViewContainer',
+ template: "mail.ListFieldActivityViewContainer",
});
-registry.category('fields').add('list_activity', ListFieldActivityViewContainer);
+registry.category("fields").add("list_activity", ListFieldActivityViewContainer);
diff --git a/addons/mail/static/src/component_hooks/use_component_to_model.js b/addons/mail/static/src/component_hooks/use_component_to_model.js
index 94ef621ec8b..82681287cb4 100644
--- a/addons/mail/static/src/component_hooks/use_component_to_model.js
+++ b/addons/mail/static/src/component_hooks/use_component_to_model.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import { clear } from '@mail/model';
+import { clear } from "@mail/model";
-import { onWillUpdateProps, useComponent } from '@odoo/owl';
+import { onWillUpdateProps, useComponent } from "@odoo/owl";
/**
* This hook provides support for saving the reference of the component directly
@@ -15,7 +15,7 @@ import { onWillUpdateProps, useComponent } from '@odoo/owl';
export function useComponentToModel({ fieldName }) {
const component = useComponent();
component.props.record.update({ [fieldName]: component });
- onWillUpdateProps(nextProps => {
+ onWillUpdateProps((nextProps) => {
const currentRecord = component.props.record;
const nextRecord = nextProps.record;
if (currentRecord.exists() && currentRecord !== nextRecord) {
diff --git a/addons/mail/static/src/component_hooks/use_messaging_container.js b/addons/mail/static/src/component_hooks/use_messaging_container.js
index e89e0a6223b..3d4fc0e476f 100644
--- a/addons/mail/static/src/component_hooks/use_messaging_container.js
+++ b/addons/mail/static/src/component_hooks/use_messaging_container.js
@@ -1,16 +1,16 @@
/** @odoo-module **/
-import { useModels } from '@mail/component_hooks/use_models';
+import { useModels } from "@mail/component_hooks/use_models";
import { componentRegistry, getMessagingComponent } from "@mail/utils/messaging_component";
-import { useComponent } from '@odoo/owl';
+import { useComponent } from "@odoo/owl";
export function useMessagingContainer() {
const component = useComponent();
component.isLoaded = false;
useModels();
component.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
- let components = { ...component.constructor.components };
+ const components = { ...component.constructor.components };
for (const name in componentRegistry) {
Object.assign(components, { [name]: getMessagingComponent(name) });
}
diff --git a/addons/mail/static/src/component_hooks/use_models.js b/addons/mail/static/src/component_hooks/use_models.js
index 7bfad8a2ed7..45223d93d61 100644
--- a/addons/mail/static/src/component_hooks/use_models.js
+++ b/addons/mail/static/src/component_hooks/use_models.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import { Listener } from '@mail/model/model_listener';
+import { Listener } from "@mail/model/model_listener";
-import { onRendered, onWillDestroy, onWillRender, useComponent } from '@odoo/owl';
+import { onRendered, onWillDestroy, onWillRender, useComponent } from "@odoo/owl";
/**
* This hook provides support for automatically re-rendering when used records
diff --git a/addons/mail/static/src/component_hooks/use_ref_to_model.js b/addons/mail/static/src/component_hooks/use_ref_to_model.js
index 9f7971c836e..97284e7e54b 100644
--- a/addons/mail/static/src/component_hooks/use_ref_to_model.js
+++ b/addons/mail/static/src/component_hooks/use_ref_to_model.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import { clear } from '@mail/model';
+import { clear } from "@mail/model";
-import { onWillUpdateProps, useComponent, useRef } from '@odoo/owl';
+import { onWillUpdateProps, useComponent, useRef } from "@odoo/owl";
/**
* This hook provides support for saving the result of useRef directly into the
@@ -17,7 +17,7 @@ export function useRefToModel({ fieldName, refName }) {
const component = useComponent();
const ref = useRef(refName);
component.props.record.update({ [fieldName]: ref });
- onWillUpdateProps(nextProps => {
+ onWillUpdateProps((nextProps) => {
const currentRecord = component.props.record;
const nextRecord = nextProps.record;
if (currentRecord.exists() && currentRecord !== nextRecord) {
diff --git a/addons/mail/static/src/component_hooks/use_update.js b/addons/mail/static/src/component_hooks/use_update.js
index 6bc60ab4183..54434434f19 100644
--- a/addons/mail/static/src/component_hooks/use_update.js
+++ b/addons/mail/static/src/component_hooks/use_update.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import { Listener } from '@mail/model/model_listener';
+import { Listener } from "@mail/model/model_listener";
-import { onMounted, onPatched, onWillDestroy, useComponent } from '@odoo/owl';
+import { onMounted, onPatched, onWillDestroy, useComponent } from "@odoo/owl";
/**
* This hook provides support for executing code after update (render or patch).
diff --git a/addons/mail/static/src/component_hooks/use_update_to_model.js b/addons/mail/static/src/component_hooks/use_update_to_model.js
index a05b81c5ee3..f8337a74466 100644
--- a/addons/mail/static/src/component_hooks/use_update_to_model.js
+++ b/addons/mail/static/src/component_hooks/use_update_to_model.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import { useUpdate } from '@mail/component_hooks/use_update';
+import { useUpdate } from "@mail/component_hooks/use_update";
-import { useComponent } from '@odoo/owl';
+import { useComponent } from "@odoo/owl";
/**
* This hook provides support for binding the onMounted/onPatched hooks to the
@@ -13,7 +13,9 @@ import { useComponent } from '@odoo/owl';
*/
export function useUpdateToModel({ methodName }) {
const component = useComponent();
- useUpdate({ func: () => {
- component.props.record[methodName]();
- } });
+ useUpdate({
+ func: () => {
+ component.props.record[methodName]();
+ },
+ });
}
diff --git a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js
index a960a22cfeb..a2679915715 100644
--- a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js
+++ b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js
@@ -1,28 +1,28 @@
/** @odoo-module **/
// ensure components are registered beforehand.
-import '@mail/components/activity_menu_view/activity_menu_view';
+import "@mail/components/activity_menu_view/activity_menu_view";
import { getMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class ActivityMenuContainer extends Component {
-
/**
* @override
*/
setup() {
super.setup();
this.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
- this.activityMenuView = this.env.services.messaging.modelManager.messaging.models['ActivityMenuView'].insert();
+ this.activityMenuView = this.env.services.messaging.modelManager.messaging.models[
+ "ActivityMenuView"
+ ].insert();
this.render();
});
}
-
}
ActivityMenuContainer.props = {};
Object.assign(ActivityMenuContainer, {
- components: { ActivityMenuView: getMessagingComponent('ActivityMenuView') },
- template: 'mail.ActivityMenuContainer',
+ components: { ActivityMenuView: getMessagingComponent("ActivityMenuView") },
+ template: "mail.ActivityMenuContainer",
});
diff --git a/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js
index cda1efa9e45..e482fe5607e 100644
--- a/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js
+++ b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js
@@ -1,17 +1,17 @@
/** @odoo-module **/
-import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class ActivityMenuView extends Component {
/**
* @override
*/
- setup() {
+ setup() {
super.setup();
- useComponentToModel({ fieldName: 'component' });
+ useComponentToModel({ fieldName: "component" });
}
/**
* @returns {ActivityMenuView}
@@ -23,7 +23,7 @@ export class ActivityMenuView extends Component {
Object.assign(ActivityMenuView, {
props: { record: Object },
- template: 'mail.ActivityMenuView',
+ template: "mail.ActivityMenuView",
});
registerMessagingComponent(ActivityMenuView);
diff --git a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js
index c656bbdd950..2a95ab5a120 100644
--- a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js
+++ b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js
@@ -1,24 +1,23 @@
/** @odoo-module **/
-import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
-import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
-import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
+import { useRefToModel } from "@mail/component_hooks/use_ref_to_model";
+import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model";
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
-import { Component, onMounted } from '@odoo/owl';
+import { Component, onMounted } from "@odoo/owl";
export class AttachmentViewer extends Component {
-
/**
* @override
*/
setup() {
super.setup();
- useComponentToModel({ fieldName: 'component' });
- useRefToModel({ fieldName: 'imageRef', refName: 'image' });
- useRefToModel({ fieldName: 'zoomerRef', refName: 'zoomer' });
- useRefToModel({ fieldName: 'iframeViewerPdfRef', refName: 'iframeViewerPdf' });
- useUpdateToModel({ methodName: 'onComponentUpdate' });
+ useComponentToModel({ fieldName: "component" });
+ useRefToModel({ fieldName: "imageRef", refName: "image" });
+ useRefToModel({ fieldName: "zoomerRef", refName: "zoomer" });
+ useRefToModel({ fieldName: "iframeViewerPdfRef", refName: "iframeViewerPdf" });
+ useUpdateToModel({ methodName: "onComponentUpdate" });
onMounted(() => this._mounted());
}
@@ -39,12 +38,11 @@ export class AttachmentViewer extends Component {
get attachmentViewer() {
return this.props.record;
}
-
}
Object.assign(AttachmentViewer, {
props: { record: Object },
- template: 'mail.AttachmentViewer',
+ template: "mail.AttachmentViewer",
});
registerMessagingComponent(AttachmentViewer);
diff --git a/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js b/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js
index 630dc25f1c6..00f411819c8 100644
--- a/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js
+++ b/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js
@@ -1,14 +1,14 @@
/** @odoo-module **/
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class CallInviteRequestPopupList extends Component {}
Object.assign(CallInviteRequestPopupList, {
props: {},
- template: 'mail.CallInviteRequestPopupList',
+ template: "mail.CallInviteRequestPopupList",
});
registerMessagingComponent(CallInviteRequestPopupList);
diff --git a/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js b/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js
index 8d5af64a726..e0b760cf621 100644
--- a/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js
+++ b/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js
@@ -1,14 +1,14 @@
/** @odoo-module **/
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class CallSettingsMenuDevice extends Component {}
Object.assign(CallSettingsMenuDevice, {
props: { device: Object },
- template: 'mail.CallSettingsMenuDevice',
+ template: "mail.CallSettingsMenuDevice",
});
registerMessagingComponent(CallSettingsMenuDevice);
diff --git a/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js b/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js
index 920c0d4af23..7fa29354925 100644
--- a/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js
+++ b/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js
@@ -1,11 +1,10 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class CallSystrayMenuContainer extends Component {
-
/**
* @override
*/
@@ -16,10 +15,9 @@ export class CallSystrayMenuContainer extends Component {
get messaging() {
return this.env.services.messaging.modelManager.messaging;
}
-
}
CallSystrayMenuContainer.props = {};
Object.assign(CallSystrayMenuContainer, {
- template: 'mail.CallSystrayMenuContainer',
+ template: "mail.CallSystrayMenuContainer",
});
diff --git a/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js b/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js
index c27e5dce8f8..de34f066771 100644
--- a/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js
+++ b/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js
@@ -1,11 +1,10 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class ChatWindowManagerContainer extends Component {
-
/**
* @override
*/
@@ -20,5 +19,5 @@ export class ChatWindowManagerContainer extends Component {
ChatWindowManagerContainer.props = {};
Object.assign(ChatWindowManagerContainer, {
- template: 'mail.ChatWindowManagerContainer',
+ template: "mail.ChatWindowManagerContainer",
});
diff --git a/addons/mail/static/src/components/chatter_container/chatter_container.js b/addons/mail/static/src/components/chatter_container/chatter_container.js
index bce73b478e6..6e70645db10 100644
--- a/addons/mail/static/src/components/chatter_container/chatter_container.js
+++ b/addons/mail/static/src/components/chatter_container/chatter_container.js
@@ -1,9 +1,9 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
-import { clear } from '@mail/model';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
+import { clear } from "@mail/model";
-import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
+import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
export const getChatterNextTemporaryId = (function () {
let tmpId = 0;
@@ -22,7 +22,6 @@ export const getChatterNextTemporaryId = (function () {
* this component delays the mounting of chatter until it becomes initialized.
*/
export class ChatterContainer extends Component {
-
/**
* @override
*/
@@ -31,7 +30,7 @@ export class ChatterContainer extends Component {
super.setup();
this.localChatter = undefined;
this._insertFromProps(this.props);
- onWillUpdateProps(nextProps => {
+ onWillUpdateProps((nextProps) => {
this._insertFromProps(nextProps);
});
onWillDestroy(() => this.deleteLocalChatter());
@@ -67,7 +66,10 @@ export class ChatterContainer extends Component {
}
const hasToCreateChatter = !props.chatter && !this.localChatter;
if (hasToCreateChatter) {
- this.localChatter = messaging.models['Chatter'].insert({ id: getChatterNextTemporaryId(), ...values });
+ this.localChatter = messaging.models["Chatter"].insert({
+ id: getChatterNextTemporaryId(),
+ ...values,
+ });
}
const chatter = props.chatter || this.localChatter;
if (!hasToCreateChatter) {
@@ -98,7 +100,6 @@ export class ChatterContainer extends Component {
}
this.render();
}
-
}
Object.assign(ChatterContainer, {
@@ -163,7 +164,7 @@ Object.assign(ChatterContainer, {
webRecord: {
type: Object,
optional: true,
- }
+ },
},
- template: 'mail.ChatterContainer',
+ template: "mail.ChatterContainer",
});
diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js
index 4a1e3710565..8bf6cba6e33 100644
--- a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js
+++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js
@@ -1,28 +1,27 @@
/** @odoo-module **/
-import { useUpdate } from '@mail/component_hooks/use_update';
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { useUpdate } from "@mail/component_hooks/use_update";
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { useService } from "@web/core/utils/hooks";
-import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog';
+import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
-import { Component, useRef } from '@odoo/owl';
+import { Component, useRef } from "@odoo/owl";
export class ComposerSuggestedRecipientView extends Component {
-
/**
* @override
*/
setup() {
super.setup();
- this.id = _.uniqueId('o_ComposerSuggestedRecipientView_');
+ this.id = _.uniqueId("o_ComposerSuggestedRecipientView_");
useUpdate({ func: () => this._update() });
/**
* Reference of the checkbox. Useful to know whether it was checked or
* not, to properly update the corresponding state in the record or to
* prompt the user with the partner creation dialog.
*/
- this._checkboxRef = useRef('checkbox');
+ this._checkboxRef = useRef("checkbox");
this.dialogService = useService("dialog");
}
@@ -70,13 +69,17 @@ export class ComposerSuggestedRecipientView extends Component {
if (isChecked) {
this.dialogService.add(FormViewDialog, {
context: {
- active_id: this.composerSuggestedRecipientView.suggestedRecipientInfo.thread.id,
- active_model: 'mail.compose.message',
- default_email: this.composerSuggestedRecipientView.suggestedRecipientInfo.email,
- default_name: this.composerSuggestedRecipientView.suggestedRecipientInfo.name,
- default_lang: this.composerSuggestedRecipientView.suggestedRecipientInfo.lang,
+ active_id: this.composerSuggestedRecipientView.suggestedRecipientInfo.thread
+ .id,
+ active_model: "mail.compose.message",
+ default_email: this.composerSuggestedRecipientView.suggestedRecipientInfo
+ .email,
+ default_name: this.composerSuggestedRecipientView.suggestedRecipientInfo
+ .name,
+ default_lang: this.composerSuggestedRecipientView.suggestedRecipientInfo
+ .lang,
force_email: true,
- ref: 'compound_context',
+ ref: "compound_context",
},
onRecordSaved: () => this._onDialogSaved(),
resModel: "res.partner",
@@ -93,20 +96,19 @@ export class ComposerSuggestedRecipientView extends Component {
if (!this.composerSuggestedRecipientView.exists()) {
return;
}
- const thread = (
+ const thread =
this.composerSuggestedRecipientView.suggestedRecipientInfo &&
- this.composerSuggestedRecipientView.suggestedRecipientInfo.thread
- );
+ this.composerSuggestedRecipientView.suggestedRecipientInfo.thread;
if (!thread) {
return;
}
- thread.fetchData(['suggestedRecipients']);
+ thread.fetchData(["suggestedRecipients"]);
}
}
Object.assign(ComposerSuggestedRecipientView, {
props: { record: Object },
- template: 'mail.ComposerSuggestedRecipientView',
+ template: "mail.ComposerSuggestedRecipientView",
});
registerMessagingComponent(ComposerSuggestedRecipientView);
diff --git a/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js b/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js
index 7f2206859e4..de7be9fe065 100644
--- a/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js
+++ b/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js
@@ -1,15 +1,14 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class DialogManagerContainer extends Component {
-
/**
* @override
*/
- setup() {
+ setup() {
useMessagingContainer();
}
@@ -20,5 +19,5 @@ export class DialogManagerContainer extends Component {
DialogManagerContainer.props = {};
Object.assign(DialogManagerContainer, {
- template: 'mail.DialogManagerContainer',
+ template: "mail.DialogManagerContainer",
});
diff --git a/addons/mail/static/src/components/discuss_container/discuss_container.js b/addons/mail/static/src/components/discuss_container/discuss_container.js
index 39218b6d8d1..c4dd217c48c 100644
--- a/addons/mail/static/src/components/discuss_container/discuss_container.js
+++ b/addons/mail/static/src/components/discuss_container/discuss_container.js
@@ -1,11 +1,10 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component, onWillDestroy } from '@odoo/owl';
+import { Component, onWillDestroy } from "@odoo/owl";
export class DiscussContainer extends Component {
-
/**
* @override
*/
@@ -18,7 +17,7 @@ export class DiscussContainer extends Component {
const initActiveId =
(action.context && action.context.active_id) ||
(action.params && action.params.default_active_id) ||
- 'mail.box_inbox';
+ "mail.box_inbox";
this.discuss = this.messaging.discuss;
this.discuss.update({
discussView: {
@@ -58,7 +57,6 @@ export class DiscussContainer extends Component {
this.discuss.close();
}
}
-
}
Object.assign(DiscussContainer, {
@@ -68,5 +66,5 @@ Object.assign(DiscussContainer, {
className: String,
globalState: { type: Object, optional: 1 },
},
- template: 'mail.DiscussContainer',
+ template: "mail.DiscussContainer",
});
diff --git a/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js b/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js
index d7382c7a15c..99a7a931da8 100644
--- a/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js
+++ b/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js
@@ -1,20 +1,23 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class DiscussPublicViewContainer extends Component {
-
/**
* @override
*/
setup() {
super.setup();
useMessagingContainer();
- this.env.services.messaging.get().then(messaging => {
- messaging.models['Thread'].insert(messaging.models['Thread'].convertData(this.props.data.channelData));
- this.discussPublicView = messaging.models['DiscussPublicView'].insert(this.props.data.discussPublicViewData);
+ this.env.services.messaging.get().then((messaging) => {
+ messaging.models["Thread"].insert(
+ messaging.models["Thread"].convertData(this.props.data.channelData)
+ );
+ this.discussPublicView = messaging.models["DiscussPublicView"].insert(
+ this.props.data.discussPublicViewData
+ );
if (this.discussPublicView.shouldDisplayWelcomeViewInitially) {
this.discussPublicView.switchToWelcomeView();
} else {
@@ -23,11 +26,10 @@ export class DiscussPublicViewContainer extends Component {
this.render();
});
}
-
}
Object.assign(DiscussPublicViewContainer, {
- template: 'mail.DiscussPublicViewContainer',
+ template: "mail.DiscussPublicViewContainer",
props: {
data: Object,
},
diff --git a/addons/mail/static/src/components/message_list/message_list.js b/addons/mail/static/src/components/message_list/message_list.js
index ae6f3da12ed..d103d94c9b7 100644
--- a/addons/mail/static/src/components/message_list/message_list.js
+++ b/addons/mail/static/src/components/message_list/message_list.js
@@ -1,24 +1,23 @@
/** @odoo-module **/
-import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
-import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
-import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
+import { useRefToModel } from "@mail/component_hooks/use_ref_to_model";
+import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model";
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Transition } from "@web/core/transition";
-import { Component, onWillPatch } from '@odoo/owl';
+import { Component, onWillPatch } from "@odoo/owl";
export class MessageListView extends Component {
-
/**
* @override
*/
setup() {
super.setup();
- useComponentToModel({ fieldName: 'component' });
- useRefToModel({ fieldName: 'loadMoreRef', refName: 'loadMore' });
- useUpdateToModel({ methodName: 'onComponentUpdate' });
+ useComponentToModel({ fieldName: "component" });
+ useRefToModel({ fieldName: "loadMoreRef", refName: "loadMore" });
+ useUpdateToModel({ methodName: "onComponentUpdate" });
/**
* Snapshot computed during willPatch, which is used by patched.
*/
@@ -46,13 +45,12 @@ export class MessageListView extends Component {
get messageListView() {
return this.props.record;
}
-
}
Object.assign(MessageListView, {
components: { Transition },
props: { record: Object },
- template: 'mail.MessageListView',
+ template: "mail.MessageListView",
});
registerMessagingComponent(MessageListView);
diff --git a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js
index afeaab92037..3ec753133b2 100644
--- a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js
+++ b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js
@@ -1,11 +1,10 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class MessagingMenuContainer extends Component {
-
/**
* @override
*/
@@ -20,5 +19,5 @@ export class MessagingMenuContainer extends Component {
MessagingMenuContainer.props = {};
Object.assign(MessagingMenuContainer, {
- template: 'mail.MessagingMenuContainer',
+ template: "mail.MessagingMenuContainer",
});
diff --git a/addons/mail/static/src/components/notification_alert/notification_alert.js b/addons/mail/static/src/components/notification_alert/notification_alert.js
index 78332f58e2a..e1c6d9056c0 100644
--- a/addons/mail/static/src/components/notification_alert/notification_alert.js
+++ b/addons/mail/static/src/components/notification_alert/notification_alert.js
@@ -1,11 +1,11 @@
/** @odoo-module **/
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class NotificationAlert extends Component {}
Object.assign(NotificationAlert, {
- template: 'mail.NotificationAlert',
+ template: "mail.NotificationAlert",
});
registerMessagingComponent(NotificationAlert);
diff --git a/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js b/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js
index f63566c3b4c..c53f81d2732 100644
--- a/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js
+++ b/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js
@@ -3,19 +3,16 @@
import { registry } from "@web/core/registry";
import { getMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class NotificationAlertContainer extends Component {}
Object.assign(NotificationAlertContainer, {
- components: { NotificationAlert: getMessagingComponent('NotificationAlert') },
- template: 'mail.NotificationAlertContainer',
+ components: { NotificationAlert: getMessagingComponent("NotificationAlert") },
+ template: "mail.NotificationAlertContainer",
props: {
// Should call standard_widget_props.js
readonly: { type: Boolean, optional: true },
record: { type: Object },
},
});
-registry.category("view_widgets").add(
- "notification_alert",
- NotificationAlertContainer
-);
+registry.category("view_widgets").add("notification_alert", NotificationAlertContainer);
diff --git a/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js b/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js
index f2eafcb2ce2..d06a631864a 100644
--- a/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js
+++ b/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js
@@ -1,11 +1,10 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class PopoverManagerContainer extends Component {
-
/**
* @override
*/
@@ -20,5 +19,5 @@ export class PopoverManagerContainer extends Component {
PopoverManagerContainer.props = {};
Object.assign(PopoverManagerContainer, {
- template: 'mail.PopoverManagerContainer',
+ template: "mail.PopoverManagerContainer",
});
diff --git a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js
index bd204b44a50..b88302b07cf 100644
--- a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js
+++ b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js
@@ -1,18 +1,17 @@
/** @odoo-module **/
-import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { useRefToModel } from "@mail/component_hooks/use_ref_to_model";
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class ThreadNeedactionPreviewView extends Component {
-
/**
* @override
*/
setup() {
super.setup();
- useRefToModel({ fieldName: 'markAsReadRef', refName: 'markAsRead' });
+ useRefToModel({ fieldName: "markAsReadRef", refName: "markAsRead" });
}
//--------------------------------------------------------------------------
@@ -29,7 +28,7 @@ export class ThreadNeedactionPreviewView extends Component {
return this.threadNeedactionPreviewView.thread.moduleIcon;
}
if (!this.threadNeedactionPreviewView.thread.channel) {
- return '/mail/static/src/img/smiley/avatar.jpg';
+ return "/mail/static/src/img/smiley/avatar.jpg";
}
if (this.threadNeedactionPreviewView.thread.channel.correspondent) {
return this.threadNeedactionPreviewView.thread.channel.correspondent.avatarUrl;
@@ -43,12 +42,11 @@ export class ThreadNeedactionPreviewView extends Component {
get threadNeedactionPreviewView() {
return this.props.record;
}
-
}
Object.assign(ThreadNeedactionPreviewView, {
props: { record: Object },
- template: 'mail.ThreadNeedactionPreviewView',
+ template: "mail.ThreadNeedactionPreviewView",
});
registerMessagingComponent(ThreadNeedactionPreviewView);
diff --git a/addons/mail/static/src/components/tracking_value/tracking_value.js b/addons/mail/static/src/components/tracking_value/tracking_value.js
index 9915a11f163..3633e44c8bd 100644
--- a/addons/mail/static/src/components/tracking_value/tracking_value.js
+++ b/addons/mail/static/src/components/tracking_value/tracking_value.js
@@ -1,23 +1,21 @@
/** @odoo-module **/
-import { registerMessagingComponent } from '@mail/utils/messaging_component';
+import { registerMessagingComponent } from "@mail/utils/messaging_component";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
export class TrackingValue extends Component {
-
/**
* @returns {TrackingValue}
*/
get trackingValue() {
return this.props.value;
}
-
}
Object.assign(TrackingValue, {
props: { value: Object },
- template: 'mail.TrackingValue',
+ template: "mail.TrackingValue",
});
registerMessagingComponent(TrackingValue);
diff --git a/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js b/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js
index f8cd0c4ddc6..2c08f9b32de 100644
--- a/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js
+++ b/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
+import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
-import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
+import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -22,7 +22,6 @@ const getNextId = (function () {
* mounting of attachment view until it becomes initialized.
*/
export class WebClientViewAttachmentViewContainer extends Component {
-
/**
* @override
*/
@@ -32,7 +31,7 @@ export class WebClientViewAttachmentViewContainer extends Component {
this.webClientViewAttachmentView = undefined;
this.webClientViewAttachmentViewId = getNextId();
this._insertFromProps(this.props);
- onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
+ onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -61,17 +60,16 @@ export class WebClientViewAttachmentViewContainer extends Component {
this._deleteRecord();
return;
}
- const thread = messaging.models['Thread'].insert({
+ const thread = messaging.models["Thread"].insert({
id: props.threadId,
model: props.threadModel,
});
- this.webClientViewAttachmentView = messaging.models['WebClientViewAttachmentView'].insert({
+ this.webClientViewAttachmentView = messaging.models["WebClientViewAttachmentView"].insert({
id: this.webClientViewAttachmentViewId,
thread,
});
this.render();
}
-
}
Object.assign(WebClientViewAttachmentViewContainer, {
@@ -82,5 +80,5 @@ Object.assign(WebClientViewAttachmentViewContainer, {
},
threadModel: String,
},
- template: 'mail.WebClientViewAttachmentViewContainer',
+ template: "mail.WebClientViewAttachmentViewContainer",
});
diff --git a/addons/mail/static/src/core_models/device.js b/addons/mail/static/src/core_models/device.js
index cd41c4a50d7..560b0323314 100644
--- a/addons/mail/static/src/core_models/device.js
+++ b/addons/mail/static/src/core_models/device.js
@@ -1,19 +1,19 @@
/** @odoo-module **/
-import { attr, Model } from '@mail/model';
+import { attr, Model } from "@mail/model";
import { browser } from "@web/core/browser/browser";
import { isMobileOS } from "@web/core/browser/feature_detection";
-import { SIZES } from '@web/core/ui/ui_service';
+import { SIZES } from "@web/core/ui/ui_service";
Model({
- name: 'Device',
+ name: "Device",
lifecycleHooks: {
_created() {
this._refresh();
this._onResize = _.debounce(() => this._refresh(), 100);
},
_willDelete() {
- browser.removeEventListener('resize', this._onResize);
+ browser.removeEventListener("resize", this._onResize);
},
},
recordMethods: {
@@ -21,7 +21,7 @@ Model({
* Called when messaging is started.
*/
start() {
- browser.addEventListener('resize', this._onResize);
+ browser.addEventListener("resize", this._onResize);
},
/**
* @private
@@ -39,7 +39,10 @@ Model({
fields: {
globalWindowInnerHeight: attr(),
globalWindowInnerWidth: attr(),
- hasCanvasFilterSupport: attr({ default: typeof document.createElement('canvas').getContext('2d').filter !== 'undefined' }),
+ hasCanvasFilterSupport: attr({
+ default:
+ typeof document.createElement("canvas").getContext("2d").filter !== "undefined",
+ }),
hasRtcSupport: attr({
compute() {
return Boolean(window.RTCPeerConnection && window.MediaStream);
diff --git a/addons/mail/static/src/core_models/locale.js b/addons/mail/static/src/core_models/locale.js
index 04ff51c36f5..007654afd4e 100644
--- a/addons/mail/static/src/core_models/locale.js
+++ b/addons/mail/static/src/core_models/locale.js
@@ -1,11 +1,11 @@
/** @odoo-module **/
-import { attr, Model } from '@mail/model';
+import { attr, Model } from "@mail/model";
-import { localization } from '@web/core/l10n/localization';
+import { localization } from "@web/core/l10n/localization";
Model({
- name: 'Locale',
+ name: "Locale",
fields: {
/**
* Language used by interface, formatted like {language ISO 2}_{country ISO 2} (eg: fr_FR).
diff --git a/addons/mail/static/src/core_models/messaging.js b/addons/mail/static/src/core_models/messaging.js
index bc1df7e12fe..4f50efcdb00 100644
--- a/addons/mail/static/src/core_models/messaging.js
+++ b/addons/mail/static/src/core_models/messaging.js
@@ -1,14 +1,14 @@
/** @odoo-module **/
-import { attr, many, one, Model } from '@mail/model';
-import { makeDeferred } from '@mail/utils/deferred';
+import { attr, many, one, Model } from "@mail/model";
+import { makeDeferred } from "@mail/utils/deferred";
-import { browser } from '@web/core/browser/browser';
+import { browser } from "@web/core/browser/browser";
-import { EventBus } from '@odoo/owl';
+import { EventBus } from "@odoo/owl";
Model({
- name: 'Messaging',
+ name: "Messaging",
lifecycleHooks: {
_created() {
odoo.__DEBUG__.messaging = this;
@@ -33,23 +33,35 @@ Model({
const { args = [], method, model, kwargs = {} } = params;
const { domain, fields, groupBy, ...remainingKwargs } = kwargs;
- const ormService = 'shadow' in options ? this.env.services.orm.silent : this.env.services.orm;
+ const ormService =
+ "shadow" in options ? this.env.services.orm.silent : this.env.services.orm;
switch (method) {
- case 'create': {
+ case "create": {
const { vals_list, ...createKwargs } = kwargs;
return ormService.create(model, args[0] || vals_list, createKwargs);
}
- case 'read':
+ case "read":
return ormService.read(model, args[0], args[1] || fields, remainingKwargs);
- case 'read_group':
- return ormService.readGroup(model, args[0] || domain, args[1] || fields, args[2] || groupBy, remainingKwargs);
- case 'search':
+ case "read_group":
+ return ormService.readGroup(
+ model,
+ args[0] || domain,
+ args[1] || fields,
+ args[2] || groupBy,
+ remainingKwargs
+ );
+ case "search":
return ormService.search(model, args[0] || domain, remainingKwargs);
- case 'search_read':
- return ormService.searchRead(model, args[0] || domain, args[1] || fields, remainingKwargs);
- case 'unlink':
+ case "search_read":
+ return ormService.searchRead(
+ model,
+ args[0] || domain,
+ args[1] || fields,
+ remainingKwargs
+ );
+ case "unlink":
return ormService.unlink(model, args[0], kwargs);
- case 'write': {
+ case "write": {
const { vals, ...writeKwargs } = kwargs;
return ormService.write(model, args[0], args[1] || vals, writeKwargs);
}
@@ -71,27 +83,29 @@ Model({
* Inverse of the messaging field present on all models. This field
* therefore contains all existing records.
*/
- allRecords: many('Record', { inverse: 'messaging', isCausal: true }),
+ allRecords: many("Record", { inverse: "messaging", isCausal: true }),
browser: attr({
compute() {
return browser;
},
}),
- device: one('Device', { default: {}, isCausal: true, readonly: true }),
+ device: one("Device", { default: {}, isCausal: true, readonly: true }),
/**
* Promise that will be resolved when messaging is initialized.
*/
- initializedPromise: attr({ required: true,
+ initializedPromise: attr({
+ required: true,
compute() {
return makeDeferred();
},
}),
isInitialized: attr({ default: false }),
- locale: one('Locale', { default: {}, isCausal: true, readonly: true }),
+ locale: one("Locale", { default: {}, isCausal: true, readonly: true }),
/**
* Determines the bus that is used to communicate messaging events.
*/
- messagingBus: attr({ required: true,
+ messagingBus: attr({
+ required: true,
compute() {
if (this.messagingBus) {
return; // avoid overwrite if already provided (example in tests)
diff --git a/addons/mail/static/src/core_models/record.js b/addons/mail/static/src/core_models/record.js
index 2c6d494186d..ead1395d419 100644
--- a/addons/mail/static/src/core_models/record.js
+++ b/addons/mail/static/src/core_models/record.js
@@ -1,6 +1,6 @@
/** @odoo-module **/
-import { one, Model } from '@mail/model';
+import { one, Model } from "@mail/model";
/**
* The object below defines a model. Instances of such model (or inherited
@@ -13,7 +13,7 @@ Model({
* Name of the model. Important to refer to appropriate model like in
* relational fields. Name of models must be unique.
*/
- name: 'Record',
+ name: "Record",
lifecycleHooks: {
/**
* This function is called after the record has been created, more
@@ -178,6 +178,11 @@ Model({
* States the messaging singleton. Automatically assigned by the model
* manager at creation.
*/
- messaging: one('Messaging', { default: {}, inverse: 'allRecords', readonly: true, required: true }),
+ messaging: one("Messaging", {
+ default: {},
+ inverse: "allRecords",
+ readonly: true,
+ required: true,
+ }),
},
});
diff --git a/addons/mail/static/src/core_models/timer.js b/addons/mail/static/src/core_models/timer.js
index b0656fea5df..9da0ce36fbd 100644
--- a/addons/mail/static/src/core_models/timer.js
+++ b/addons/mail/static/src/core_models/timer.js
@@ -1,13 +1,15 @@
/** @odoo-module **/
-import { attr, clear, Model } from '@mail/model';
+import { attr, clear, Model } from "@mail/model";
Model({
- name: 'Timer',
- identifyingMode: 'xor',
+ name: "Timer",
+ identifyingMode: "xor",
lifecycleHooks: {
_created() {
- this.update({ timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration) });
+ this.update({
+ timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration),
+ });
},
_willDelete() {
this.messaging.browser.clearTimeout(this.timeoutId);
@@ -39,7 +41,8 @@ Model({
* Duration, in milliseconds, until timer times out and calls the
* timeout function.
*/
- duration: attr({ required: true,
+ duration: attr({
+ required: true,
compute() {
return clear();
},
@@ -53,7 +56,7 @@ Model({
onChanges: [
{
dependencies: ["doReset"],
- methodName: '_onChangeDoReset',
+ methodName: "_onChangeDoReset",
},
],
});
diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js
index f60fc3f9678..20b2de3c972 100644
--- a/addons/mail/static/src/js/activity.js
+++ b/addons/mail/static/src/js/activity.js
@@ -1,13 +1,13 @@
/** @odoo-module **/
-import AbstractField from 'web.AbstractField';
-import config from 'web.config';
-import core from 'web.core';
-import field_registry from 'web.field_registry';
-import session from 'web.session';
-import { sprintf } from '@web/core/utils/strings';
-import framework from 'web.framework';
-import time from 'web.time';
+import AbstractField from "web.AbstractField";
+import config from "web.config";
+import core from "web.core";
+import field_registry from "web.field_registry";
+import session from "web.session";
+import { sprintf } from "@web/core/utils/strings";
+import framework from "web.framework";
+import time from "web.time";
const QWeb = core.qweb;
const _t = core._t;
@@ -17,21 +17,21 @@ const _lt = core._lt;
// Activities Widget for Kanban views ('kanban_activity' widget)
// -----------------------------------------------------------------------------
const KanbanActivity = AbstractField.extend({
- template: 'mail.KanbanActivity',
+ template: "mail.KanbanActivity",
events: {
- 'change input.o_input_file': '_onFileChanged',
- 'click .o_activity_template_preview': '_onPreviewMailTemplate',
- 'click .o_activity_template_send': '_onSendMailTemplate',
- 'click .o_edit_activity': '_onEditActivity',
- 'click .o_mark_as_done': '_onMarkActivityDone',
- 'click .o_mark_as_done_upload_file': '_onMarkActivityDoneUploadFile',
- 'click .o_schedule_activity': '_onScheduleActivity',
- 'show.bs.dropdown': '_onDropdownShow',
+ "change input.o_input_file": "_onFileChanged",
+ "click .o_activity_template_preview": "_onPreviewMailTemplate",
+ "click .o_activity_template_send": "_onSendMailTemplate",
+ "click .o_edit_activity": "_onEditActivity",
+ "click .o_mark_as_done": "_onMarkActivityDone",
+ "click .o_mark_as_done_upload_file": "_onMarkActivityDoneUploadFile",
+ "click .o_schedule_activity": "_onScheduleActivity",
+ "show.bs.dropdown": "_onDropdownShow",
},
fieldDependencies: {
- activity_exception_decoration: { type: 'selection' },
- activity_exception_icon: { type: 'char' },
- activity_state: { type: 'selection' },
+ activity_exception_decoration: { type: "selection" },
+ activity_exception_icon: { type: "char" },
+ activity_state: { type: "selection" },
},
/**
@@ -72,8 +72,8 @@ const KanbanActivity = AbstractField.extend({
const feedback = params.feedback || false;
const attachmentIds = params.attachmentIds || [];
await this._rpc({
- model: 'mail.activity',
- method: 'action_feedback',
+ model: "mail.activity",
+ method: "action_feedback",
args: [[activityID]],
kwargs: {
feedback: feedback,
@@ -97,8 +97,8 @@ const KanbanActivity = AbstractField.extend({
const activityID = params.activityID;
const feedback = params.feedback;
const action = await this._rpc({
- model: 'mail.activity',
- method: 'action_feedback_schedule_next',
+ model: "mail.activity",
+ method: "action_feedback_schedule_next",
args: [[activityID]],
kwargs: { feedback: feedback },
context: this.record.getContext(),
@@ -106,11 +106,11 @@ const KanbanActivity = AbstractField.extend({
if (action) {
this.do_action(action, {
on_close: () => {
- this.trigger_up('reload', { keepChanges: true });
+ this.trigger_up("reload", { keepChanges: true });
},
});
} else {
- this.trigger_up('reload', { keepChanges: true });
+ this.trigger_up("reload", { keepChanges: true });
}
},
/**
@@ -120,9 +120,12 @@ const KanbanActivity = AbstractField.extend({
*/
async _onEditActivity(ev) {
ev.preventDefault();
- const activityId = $(ev.currentTarget).data('activity-id');
- const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model });
- const activity = this.messaging.models['Activity'].insert({ id: activityId, thread });
+ const activityId = $(ev.currentTarget).data("activity-id");
+ const thread = this.messaging.models["Thread"].insert({
+ id: this.res_id,
+ model: this.model,
+ });
+ const activity = this.messaging.models["Activity"].insert({ id: activityId, thread });
await this.messaging.openActivityForm({
activity,
defaultActivityTypeId: this.defaultActivityType,
@@ -137,7 +140,7 @@ const KanbanActivity = AbstractField.extend({
_onFileChanged(ev) {
ev.stopPropagation();
ev.preventDefault();
- const $form = $(ev.currentTarget).closest('form');
+ const $form = $(ev.currentTarget).closest("form");
$form.submit();
framework.blockUI();
},
@@ -157,143 +160,157 @@ const KanbanActivity = AbstractField.extend({
ev.preventDefault();
const self = this;
const $markDoneBtn = $(ev.currentTarget);
- const activityID = $markDoneBtn.data('activity-id');
- const previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id') || false;
- const chainingTypeActivity = $markDoneBtn.data('chaining-type-activity');
+ const activityID = $markDoneBtn.data("activity-id");
+ const previousActivityTypeID = $markDoneBtn.data("previous-activity-type-id") || false;
+ const chainingTypeActivity = $markDoneBtn.data("chaining-type-activity");
- if ($markDoneBtn[0].dataset.bsToggle === 'collapse') {
- const $actLi = $markDoneBtn.parents('.o_log_activity');
- const $panel = this.$('#o_activity_form_' + activityID);
+ if ($markDoneBtn[0].dataset.bsToggle === "collapse") {
+ const $actLi = $markDoneBtn.parents(".o_log_activity");
+ const $panel = this.$("#o_activity_form_" + activityID);
- if (!$panel.data('bs.collapse')) {
- const $form = $(QWeb.render('mail.activity_feedback_form', {
- previous_activity_type_id: previousActivityTypeID,
- chaining_type: chainingTypeActivity
- }));
+ if (!$panel.data("bs.collapse")) {
+ const $form = $(
+ QWeb.render("mail.activity_feedback_form", {
+ previous_activity_type_id: previousActivityTypeID,
+ chaining_type: chainingTypeActivity,
+ })
+ );
$panel.append($form);
this._onMarkActivityDoneActions($markDoneBtn, $form, activityID);
// Close and reset any other open panels
- _.each($panel.siblings('.o_activity_form'), function (el) {
- if ($(el).data('bs.collapse')) {
- $(el).empty().collapse('dispose').removeClass('show');
+ _.each($panel.siblings(".o_activity_form"), function (el) {
+ if ($(el).data("bs.collapse")) {
+ $(el).empty().collapse("dispose").removeClass("show");
}
- $(el).data('bs.collapse', false);
+ $(el).data("bs.collapse", false);
});
// Scroll to selected activity
- $markDoneBtn.parents('.o_activity_log_container').scrollTo($actLi.position().top, 100);
- $panel.data('bs.collapse', true);
+ $markDoneBtn
+ .parents(".o_activity_log_container")
+ .scrollTo($actLi.position().top, 100);
+ $panel.data("bs.collapse", true);
}
// Empty and reset panel on close
- $panel.on('hidden.bs.collapse', function () {
- if ($panel.data('bs.collapse')) {
- $actLi.removeClass('o_activity_selected');
- $panel.collapse('dispose');
+ $panel.on("hidden.bs.collapse", function () {
+ if ($panel.data("bs.collapse")) {
+ $actLi.removeClass("o_activity_selected");
+ $panel.collapse("dispose");
$panel.empty();
}
- $panel.data('bs.collapse', false);
+ $panel.data("bs.collapse", false);
});
- this.$('.o_activity_selected').removeClass('o_activity_selected');
- $actLi.toggleClass('o_activity_selected');
- $panel.collapse('toggle');
- $panel.find('#activity_feedback').focus();
-
- } else if (!$markDoneBtn.data('bs.popover')) {
- $markDoneBtn.popover({
- template: $(Popover.Default.template).addClass('o_mail_activity_feedback')[0].outerHTML, // Ugly but cannot find another way
- container: $markDoneBtn,
- title: _t("Feedback"),
- html: true,
- trigger: 'manual',
- placement: 'right', // FIXME: this should work, maybe a bug in the popper lib
- content: () => {
- const $popover = $(QWeb.render('mail.activity_feedback_form', {
- previous_activity_type_id: previousActivityTypeID,
- chaining_type: chainingTypeActivity
- }));
- this._onMarkActivityDoneActions($markDoneBtn, $popover, activityID);
- return $popover;
- },
- }).on('shown.bs.popover', function () {
- const $popover = $($(this).data("bs.popover").tip);
- $(".o_mail_activity_feedback.popover").not($popover).popover("hide");
- $popover.addClass('o_mail_activity_feedback').attr('tabindex', 0);
- $popover.find('#activity_feedback').focus();
- const activityID = $(this).data('activity-id');
- $popover.off('focusout');
- $popover.focusout(e => {
- // outside click of popover hide the popover
- // e.relatedTarget is the element receiving the focus
- if (!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) {
- self._draftFeedback[activityID] = $popover.find('#activity_feedback').val();
- $popover.popover('hide');
- }
- });
- }).popover('show');
+ this.$(".o_activity_selected").removeClass("o_activity_selected");
+ $actLi.toggleClass("o_activity_selected");
+ $panel.collapse("toggle");
+ $panel.find("#activity_feedback").focus();
+ } else if (!$markDoneBtn.data("bs.popover")) {
+ $markDoneBtn
+ .popover({
+ template: $(Popover.Default.template).addClass("o_mail_activity_feedback")[0]
+ .outerHTML, // Ugly but cannot find another way
+ container: $markDoneBtn,
+ title: _t("Feedback"),
+ html: true,
+ trigger: "manual",
+ placement: "right", // FIXME: this should work, maybe a bug in the popper lib
+ content: () => {
+ const $popover = $(
+ QWeb.render("mail.activity_feedback_form", {
+ previous_activity_type_id: previousActivityTypeID,
+ chaining_type: chainingTypeActivity,
+ })
+ );
+ this._onMarkActivityDoneActions($markDoneBtn, $popover, activityID);
+ return $popover;
+ },
+ })
+ .on("shown.bs.popover", function () {
+ const $popover = $($(this).data("bs.popover").tip);
+ $(".o_mail_activity_feedback.popover").not($popover).popover("hide");
+ $popover.addClass("o_mail_activity_feedback").attr("tabindex", 0);
+ $popover.find("#activity_feedback").focus();
+ const activityID = $(this).data("activity-id");
+ $popover.off("focusout");
+ $popover.focusout((e) => {
+ // outside click of popover hide the popover
+ // e.relatedTarget is the element receiving the focus
+ if (
+ !$popover.is(e.relatedTarget) &&
+ !$popover.find(e.relatedTarget).length
+ ) {
+ self._draftFeedback[activityID] = $popover
+ .find("#activity_feedback")
+ .val();
+ $popover.popover("hide");
+ }
+ });
+ })
+ .popover("show");
} else {
- const popover = $markDoneBtn.data('bs.popover');
- if ($('#' + popover.tip.id).length === 0) {
- popover.show();
+ const popover = $markDoneBtn.data("bs.popover");
+ if ($("#" + popover.tip.id).length === 0) {
+ popover.show();
}
}
},
/**
- * Bind all necessary actions to the 'mark as done' form
- *
- * @private
- * @param {Object} $form
- * @param {integer} activityID
- */
+ * Bind all necessary actions to the 'mark as done' form
+ *
+ * @private
+ * @param {Object} $form
+ * @param {integer} activityID
+ */
_onMarkActivityDoneActions($btn, $form, activityID) {
- $form.find('#activity_feedback').val(this._draftFeedback[activityID]);
- $form.on('keydown', '#activity_feedback', ev => {
- if (ev.key === 'Enter') {
+ $form.find("#activity_feedback").val(this._draftFeedback[activityID]);
+ $form.on("keydown", "#activity_feedback", (ev) => {
+ if (ev.key === "Enter") {
ev.stopPropagation(); // Prevent list view actions
}
});
- $form.on('click', '.o_activity_popover_done', ev => {
+ $form.on("click", ".o_activity_popover_done", (ev) => {
ev.stopPropagation();
this._markActivityDone({
activityID: activityID,
- feedback: $form.find('#activity_feedback').val(),
+ feedback: $form.find("#activity_feedback").val(),
});
});
- $form.on('keydown', '.o_activity_popover_done', ev => {
- if (ev.key === 'Enter') {
+ $form.on("keydown", ".o_activity_popover_done", (ev) => {
+ if (ev.key === "Enter") {
ev.stopPropagation(); // Prevent list view actions
ev.preventDefault();
this._markActivityDone({
activityID,
- feedback: $form.find('#activity_feedback').val(),
+ feedback: $form.find("#activity_feedback").val(),
});
}
});
- $form.on('click', '.o_activity_popover_done_next', ev => {
+ $form.on("click", ".o_activity_popover_done_next", (ev) => {
ev.stopPropagation();
this._markActivityDoneAndScheduleNext({
activityID: activityID,
- feedback: $form.find('#activity_feedback').val(),
+ feedback: $form.find("#activity_feedback").val(),
});
});
- $form.on('keydown', '.o_activity_popover_done_next', ev => {
- if (ev.key === 'Enter') {
+ $form.on("keydown", ".o_activity_popover_done_next", (ev) => {
+ if (ev.key === "Enter") {
ev.stopPropagation(); // Prevent list view actions
ev.preventDefault();
this._markActivityDoneAndScheduleNext({
activityID,
- feedback: $form.find('#activity_feedback').val(),
+ feedback: $form.find("#activity_feedback").val(),
});
}
});
- $form.on('click', '.o_activity_popover_discard', ev => {
+ $form.on("click", ".o_activity_popover_discard", (ev) => {
ev.stopPropagation();
- if ($btn.data('bs.popover')) {
- $btn.popover('hide');
- } else if ($btn[0].dataset.bsToggle === 'collapse') {
- this.$('#o_activity_form_' + activityID).collapse('hide');
+ if ($btn.data("bs.popover")) {
+ $btn.popover("hide");
+ } else if ($btn[0].dataset.bsToggle === "collapse") {
+ this.$("#o_activity_form_" + activityID).collapse("hide");
}
});
},
@@ -304,7 +321,7 @@ const KanbanActivity = AbstractField.extend({
_onMarkActivityDoneUploadFile(ev) {
ev.preventDefault();
ev.stopPropagation();
- const fileuploadID = $(ev.currentTarget).data('fileupload-id');
+ const fileuploadID = $(ev.currentTarget).data("fileupload-id");
const $input = this.$("[target='" + fileuploadID + "'] > input.o_input_file");
$input.click();
},
@@ -316,13 +333,13 @@ const KanbanActivity = AbstractField.extend({
_onPreviewMailTemplate(ev) {
ev.stopPropagation();
ev.preventDefault();
- const templateID = $(ev.currentTarget).data('template-id');
+ const templateID = $(ev.currentTarget).data("template-id");
const action = {
- name: _t('Compose Email'),
- type: 'ir.actions.act_window',
- res_model: 'mail.compose.message',
- views: [[false, 'form']],
- target: 'new',
+ name: _t("Compose Email"),
+ type: "ir.actions.act_window",
+ res_model: "mail.compose.message",
+ views: [[false, "form"]],
+ target: "new",
context: {
default_res_id: this.res_id,
default_model: this.model,
@@ -333,7 +350,7 @@ const KanbanActivity = AbstractField.extend({
};
return this.do_action(action, {
on_close: () => {
- this.trigger_up('reload', { keepChanges: true });
+ this.trigger_up("reload", { keepChanges: true });
},
});
},
@@ -344,7 +361,10 @@ const KanbanActivity = AbstractField.extend({
*/
async _onScheduleActivity(ev) {
ev.preventDefault();
- const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model });
+ const thread = this.messaging.models["Thread"].insert({
+ id: this.res_id,
+ model: this.model,
+ });
await this.messaging.openActivityForm({
defaultActivityTypeId: this.defaultActivityType,
thread,
@@ -359,10 +379,10 @@ const KanbanActivity = AbstractField.extend({
async _onSendMailTemplate(ev) {
ev.stopPropagation();
ev.preventDefault();
- const templateID = $(ev.currentTarget).data('template-id');
+ const templateID = $(ev.currentTarget).data("template-id");
await this._rpc({
model: this.model,
- method: 'activity_send_mail',
+ method: "activity_send_mail",
args: [[this.res_id], templateID],
});
this._reload();
@@ -371,7 +391,7 @@ const KanbanActivity = AbstractField.extend({
* @private
*/
_reload() {
- this.trigger_up('reload', { db_id: this.record.id, keepChanges: true });
+ this.trigger_up("reload", { db_id: this.record.id, keepChanges: true });
},
/**
* @override
@@ -380,16 +400,16 @@ const KanbanActivity = AbstractField.extend({
_render() {
// span classes need to be updated manually because the template cannot
// be re-rendered eaasily (because of the dropdown state)
- const spanClasses = ['fa', 'fa-lg', 'fa-fw'];
- spanClasses.push('o_activity_color_' + (this.record.data.activity_state || 'default'));
+ const spanClasses = ["fa", "fa-lg", "fa-fw"];
+ spanClasses.push("o_activity_color_" + (this.record.data.activity_state || "default"));
if (this.recordData.activity_exception_decoration) {
- spanClasses.push('text-' + this.recordData.activity_exception_decoration);
+ spanClasses.push("text-" + this.recordData.activity_exception_decoration);
spanClasses.push(this.recordData.activity_exception_icon);
} else {
- spanClasses.push('fa-clock-o');
+ spanClasses.push("fa-clock-o");
}
- this.$('.o_activity_btn > span').removeClass().addClass(spanClasses.join(' '));
- if (this.$el.hasClass('show')) {
+ this.$(".o_activity_btn > span").removeClass().addClass(spanClasses.join(" "));
+ if (this.$el.hasClass("show")) {
// note: this part of the rendering might be asynchronous
this._renderDropdown();
}
@@ -398,38 +418,42 @@ const KanbanActivity = AbstractField.extend({
* @private
*/
async _renderDropdown() {
- this.$('.o_activity')
- .toggleClass('dropdown-menu-end', config.device.isMobile)
- .html(QWeb.render('mail.KanbanActivityLoading'));
+ this.$(".o_activity")
+ .toggleClass("dropdown-menu-end", config.device.isMobile)
+ .html(QWeb.render("mail.KanbanActivityLoading"));
let context = this.getSession().user_context;
if (this.record && !_.isEmpty(this.record.getContext())) {
context = this.record.getContext();
}
- const activities = this.value.res_ids.length ? await this._rpc({
- model: 'mail.activity',
- method: 'activity_format',
- args: [this.value.res_ids],
- context: context,
- }) : [];
- const today = moment().startOf('day');
+ const activities = this.value.res_ids.length
+ ? await this._rpc({
+ model: "mail.activity",
+ method: "activity_format",
+ args: [this.value.res_ids],
+ context: context,
+ })
+ : [];
+ const today = moment().startOf("day");
for (const activity of activities) {
activity.create_date = moment(time.auto_str_to_date(activity.create_date));
activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline));
- if (activity.activity_category === 'upload_file') {
- activity.fileuploadID = _.uniqueId('o_fileupload');
+ if (activity.activity_category === "upload_file") {
+ activity.fileuploadID = _.uniqueId("o_fileupload");
}
- let toDisplay = '';
- const diff = activity.date_deadline.diff(today, 'days', true); // true means no rounding
+ let toDisplay = "";
+ const diff = activity.date_deadline.diff(today, "days", true); // true means no rounding
if (diff === 0) {
toDisplay = _t("Today");
} else {
- if (diff < 0) { // overdue
+ if (diff < 0) {
+ // overdue
if (diff === -1) {
toDisplay = _t("Yesterday");
} else {
toDisplay = sprintf(_t("%s days overdue"), Math.round(Math.abs(diff)));
}
- } else { // due
+ } else {
+ // due
if (diff === 1) {
toDisplay = _t("Tomorrow");
} else {
@@ -439,12 +463,14 @@ const KanbanActivity = AbstractField.extend({
}
activity.label_delay = toDisplay;
}
- const sortedActivities = _.sortBy(activities, 'date_deadline');
- this.$('.o_activity').html(QWeb.render('mail.KanbanActivityDropdown', {
- records: _.groupBy(sortedActivities, 'state'),
- session: session,
- widget: this,
- }));
+ const sortedActivities = _.sortBy(activities, "date_deadline");
+ this.$(".o_activity").html(
+ QWeb.render("mail.KanbanActivityDropdown", {
+ records: _.groupBy(sortedActivities, "state"),
+ session: session,
+ widget: this,
+ })
+ );
for (const activity of sortedActivities) {
if (activity.fileuploadID) {
$(window).on(activity.fileuploadID, async () => {
@@ -453,9 +479,9 @@ const KanbanActivity = AbstractField.extend({
const files = Array.prototype.slice.call(arguments, 1);
await this._markActivityDone({
activityID: activity.id,
- attachmentIds: _.pluck(files, 'id')
+ attachmentIds: _.pluck(files, "id"),
});
- this.trigger_up('reload', { keepChanges: true });
+ this.trigger_up("reload", { keepChanges: true });
});
}
}
@@ -477,16 +503,16 @@ const KanbanActivity = AbstractField.extend({
// Activities Widget for List views ('list_activity' widget)
// -----------------------------------------------------------------------------
const ListActivity = KanbanActivity.extend({
- template: 'mail.ListActivity',
+ template: "mail.ListActivity",
events: Object.assign({}, KanbanActivity.prototype.events, {
- 'click .dropdown-menu.o_activity': '_onDropdownClicked',
+ "click .dropdown-menu.o_activity": "_onDropdownClicked",
}),
fieldDependencies: _.extend({}, KanbanActivity.prototype.fieldDependencies, {
- activity_summary: { type: 'char' },
- activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
- activity_type_icon: { type: 'char' },
+ activity_summary: { type: "char" },
+ activity_type_id: { type: "many2one", relation: "mail.activity.type" },
+ activity_type_icon: { type: "char" },
}),
- label: _lt('Next Activity'),
+ label: _lt("Next Activity"),
//--------------------------------------------------------------------------
// Private
@@ -501,19 +527,22 @@ const ListActivity = KanbanActivity.extend({
// set the 'special_click' prop on the activity icon to prevent from
// opening the record when the user clicks on it (as it opens the
// activity dropdown instead)
- this.$('.o_activity_btn > span').prop('special_click', true);
+ this.$(".o_activity_btn > span").prop("special_click", true);
if (this.value.count) {
let text;
if (this.recordData.activity_exception_decoration) {
- text = _t('Warning');
+ text = _t("Warning");
} else {
- text = this.recordData.activity_summary ||
- this.recordData.activity_type_id.data.display_name;
+ text =
+ this.recordData.activity_summary ||
+ this.recordData.activity_type_id.data.display_name;
}
- this.$('.o_activity_summary').text(text);
+ this.$(".o_activity_summary").text(text);
}
if (this.recordData.activity_type_icon) {
- this.el.querySelector('.o_activity_btn > span').classList.replace('fa-clock-o', this.recordData.activity_type_icon);
+ this.el
+ .querySelector(".o_activity_btn > span")
+ .classList.replace("fa-clock-o", this.recordData.activity_type_icon);
}
},
@@ -540,7 +569,7 @@ const ListActivity = KanbanActivity.extend({
const ActivityException = AbstractField.extend({
noLabel: true,
fieldDependencies: _.extend({}, AbstractField.prototype.fieldDependencies, {
- activity_exception_icon: { type: 'char' },
+ activity_exception_icon: { type: "char" },
}),
//------------------------------------------------------------
@@ -567,14 +596,18 @@ const ActivityException = AbstractField.extend({
this.$el.empty();
if (this.value) {
this.$el.attr({
- title: _t('This record has an exception activity.'),
- class: "float-end mt-1 text-" + this.value + " fa " + this.recordData.activity_exception_icon
+ title: _t("This record has an exception activity."),
+ class:
+ "float-end mt-1 text-" +
+ this.value +
+ " fa " +
+ this.recordData.activity_exception_icon,
});
}
- }
+ },
});
field_registry
- .add('kanban_activity', KanbanActivity)
- .add('list_activity', ListActivity)
- .add('activity_exception', ActivityException);
+ .add("kanban_activity", KanbanActivity)
+ .add("list_activity", ListActivity)
+ .add("activity_exception", ActivityException);
diff --git a/addons/mail/static/src/js/basic_view.js b/addons/mail/static/src/js/basic_view.js
index 45ac2503c68..75cac9d437a 100644
--- a/addons/mail/static/src/js/basic_view.js
+++ b/addons/mail/static/src/js/basic_view.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import BasicView from 'web.BasicView';
+import BasicView from "web.BasicView";
-const mailWidgets = ['kanban_activity'];
+const mailWidgets = ["kanban_activity"];
const chatterFields = ["message_ids", "message_follower_ids", "activity_ids"];
@@ -13,19 +13,22 @@ BasicView.include({
this._super.apply(this, arguments);
this.rendererParams.hasChatter = this.hasChatter;
this.rendererParams.hasAttachmentViewerFeature = this.hasAttachmentViewerFeature;
- const post_refresh = this._getFieldOption('message_ids', 'post_refresh', false);
- const followers_post_refresh = this._getFieldOption('message_follower_ids', 'post_refresh', false);
+ const post_refresh = this._getFieldOption("message_ids", "post_refresh", false);
+ const followers_post_refresh = this._getFieldOption(
+ "message_follower_ids",
+ "post_refresh",
+ false
+ );
this.chatterFields = {
- hasActivityIds: this._hasField('activity_ids'),
- hasMessageFollowerIds: this._hasField('message_follower_ids'),
- hasMessageIds: this._hasField('message_ids'),
- hasRecordReloadOnAttachmentsChanged: post_refresh === 'always',
+ hasActivityIds: this._hasField("activity_ids"),
+ hasMessageFollowerIds: this._hasField("message_follower_ids"),
+ hasMessageIds: this._hasField("message_ids"),
+ hasRecordReloadOnAttachmentsChanged: post_refresh === "always",
hasRecordReloadOnMessagePosted: !!post_refresh,
hasRecordReloadOnFollowersUpdate: !!followers_post_refresh,
- isAttachmentBoxVisibleInitially: (
- this._getFieldOption('message_ids', 'open_attachments', false) ||
- this._getFieldOption('message_follower_ids', 'open_attachments', false)
- ),
+ isAttachmentBoxVisibleInitially:
+ this._getFieldOption("message_ids", "open_attachments", false) ||
+ this._getFieldOption("message_follower_ids", "open_attachments", false),
};
const fieldsInfo = this.fieldsInfo[this.viewType];
this.rendererParams.chatterFields = this.chatterFields;
@@ -82,7 +85,7 @@ BasicView.include({
const fieldsInfo = fv.fieldsInfo[viewType];
const fields = fv.viewFields;
for (const child of node.children) {
- if (child.tag === 'field' && chatterFields.includes(child.attrs.name)) {
+ if (child.tag === "field" && chatterFields.includes(child.attrs.name)) {
const attrs = { ...child.attrs, modifiers: {}, __no_fetch: true };
const fieldName = attrs.name;
fieldsInfo[fieldName] = this._processField(viewType, fields[fieldName], attrs);
@@ -92,7 +95,7 @@ BasicView.include({
}
return false;
}
- if (node.tag === 'div' && node.attrs.class === 'o_attachment_preview') {
+ if (node.tag === "div" && node.attrs.class === "o_attachment_preview") {
this.hasAttachmentViewerFeature = true;
}
return this._super(...arguments);
diff --git a/addons/mail/static/src/js/core/translation.js b/addons/mail/static/src/js/core/translation.js
index c89f1a2f984..e58a44e8ee8 100644
--- a/addons/mail/static/src/js/core/translation.js
+++ b/addons/mail/static/src/js/core/translation.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
-import { TranslationDataBase } from 'web.translation';
+import { TranslationDataBase } from "web.translation";
-import { Component } from '@odoo/owl';
+import { Component } from "@odoo/owl";
TranslationDataBase.include({
/**
@@ -14,7 +14,7 @@ TranslationDataBase.include({
// During boot `env.services` might not even be set yet, in this
// case this can safely be ignored as messaging will then load
// locale information during its initialization.
- Component.env.services.messaging.get().then(messaging => {
+ Component.env.services.messaging.get().then((messaging) => {
// Update messaging locale whenever the translation bundle changes.
// In particular if messaging is created before the end of the
// `load_translations` RPC, the default values have to be
diff --git a/addons/mail/static/src/js/custom_filter_item.js b/addons/mail/static/src/js/custom_filter_item.js
index 3de2c39984a..9e82dfc9b09 100644
--- a/addons/mail/static/src/js/custom_filter_item.js
+++ b/addons/mail/static/src/js/custom_filter_item.js
@@ -1,19 +1,20 @@
/** @odoo-module **/
-import CustomFilterItem from 'web.CustomFilterItem';
-import { patch } from 'web.utils';
-
-patch(CustomFilterItem.prototype, 'mail.CustomFilterItem', {
+import CustomFilterItem from "web.CustomFilterItem";
+import { patch } from "web.utils";
+patch(CustomFilterItem.prototype, "mail.CustomFilterItem", {
/**
* With the `mail` module installed, we want to filter out some of the
* available fields in 'Add custom filter' menu (@see CustomFilterItem).
* @override
*/
_validateField(field) {
- return this._super(...arguments) &&
- field.relation !== 'mail.message' &&
- field.name !== 'message_ids';
+ return (
+ this._super(...arguments) &&
+ field.relation !== "mail.message" &&
+ field.name !== "message_ids"
+ );
},
});
diff --git a/addons/mail/static/src/js/emojis.js b/addons/mail/static/src/js/emojis.js
index 6d5c8c22243..6495173cd4c 100644
--- a/addons/mail/static/src/js/emojis.js
+++ b/addons/mail/static/src/js/emojis.js
@@ -20,116 +20,116 @@
* representation in the system.
*/
const data = {
- ":)": "😊",
- ":-)": "😊", // alternative (alt.)
- "=)": "😊", // alt.
- ":]": "😊", // alt.
- ":D": "😃",
- ":-D": "😃", // alt.
- "=D": "😃", // alt.
- "xD": "😆",
- "XD": "😆", // alt.
- "x'D": "😂",
- ";)": "😉",
- ";-)": "😉", // alt.
- "B)": "😎",
- "8)": "😎", // alt.
- "B-)": "😎", // alt.
- "8-)": "😎", // alt.
- ";p": "😜",
- ";P": "😜", // alt.
- ":p": "😋",
- ":P": "😋", // alt.
- ":-p": "😋", // alt.
- ":-P": "😋", // alt.
- "=P": "😋", // alt.
- "xp": "😝",
- "xP": "😝", // alt.
- "o_o": "😳",
- ":|": "😐",
- ":-|": "😐", // alt.
- ":/": "😕", // alt.
- ":-/": "😕", // alt.
- ":(": "😞",
- ":@": "😱",
- ":O": "😲",
- ":-O": "😲", // alt.
- ":o": "😲", // alt.
- ":-o": "😲", // alt.
- ":'o": "😨",
- "3:(": "😠",
- ">:(": "😠", // alt.
- "3:)": "😈",
- ">:)": "😈", // alt.
- ":*": "😘",
- ":-*": "😘", // alt.
- "o:)": "😇",
- ":'(": "😢",
- ":'-(": "😭",
- ":\"(": "😭", // alt.
- "<3": "❤️",
- "<3": "❤️",
- ":heart": "❤️", // alt.
- "3": "💔",
- "</3": "💔",
- ":heart_eyes": "😍",
- ":turban": "👳",
- ":+1": "👍",
- ":-1": "👎",
- ":ok": "👌",
- ":poop": "💩",
- ":no_see": "🙈",
- ":no_hear": "🙉",
- ":no_speak": "🙊",
- ":bug": "🐞",
- ":kitten": "😺",
- ":bear": "🐻",
- ":snail": "🐌",
- ":boar": "🐗",
- ":clover": "🍀",
- ":sunflower": "🌹",
- ":fire": "🔥",
- ":sun": "☀️",
+ ":)": "😊",
+ ":-)": "😊", // alternative (alt.)
+ "=)": "😊", // alt.
+ ":]": "😊", // alt.
+ ":D": "😃",
+ ":-D": "😃", // alt.
+ "=D": "😃", // alt.
+ xD: "😆",
+ XD: "😆", // alt.
+ "x'D": "😂",
+ ";)": "😉",
+ ";-)": "😉", // alt.
+ "B)": "😎",
+ "8)": "😎", // alt.
+ "B-)": "😎", // alt.
+ "8-)": "😎", // alt.
+ ";p": "😜",
+ ";P": "😜", // alt.
+ ":p": "😋",
+ ":P": "😋", // alt.
+ ":-p": "😋", // alt.
+ ":-P": "😋", // alt.
+ "=P": "😋", // alt.
+ xp: "😝",
+ xP: "😝", // alt.
+ o_o: "😳",
+ ":|": "😐",
+ ":-|": "😐", // alt.
+ ":/": "😕", // alt.
+ ":-/": "😕", // alt.
+ ":(": "😞",
+ ":@": "😱",
+ ":O": "😲",
+ ":-O": "😲", // alt.
+ ":o": "😲", // alt.
+ ":-o": "😲", // alt.
+ ":'o": "😨",
+ "3:(": "😠",
+ ">:(": "😠", // alt.
+ "3:)": "😈",
+ ">:)": "😈", // alt.
+ ":*": "😘",
+ ":-*": "😘", // alt.
+ "o:)": "😇",
+ ":'(": "😢",
+ ":'-(": "😭",
+ ':"(': "😭", // alt.
+ "<3": "❤️",
+ "<3": "❤️",
+ ":heart": "❤️", // alt.
+ "3": "💔",
+ "</3": "💔",
+ ":heart_eyes": "😍",
+ ":turban": "👳",
+ ":+1": "👍",
+ ":-1": "👎",
+ ":ok": "👌",
+ ":poop": "💩",
+ ":no_see": "🙈",
+ ":no_hear": "🙉",
+ ":no_speak": "🙊",
+ ":bug": "🐞",
+ ":kitten": "😺",
+ ":bear": "🐻",
+ ":snail": "🐌",
+ ":boar": "🐗",
+ ":clover": "🍀",
+ ":sunflower": "🌹",
+ ":fire": "🔥",
+ ":sun": "☀️",
":partly_sunny:": "⛅️",
- ":rainbow": "🌈",
- ":cloud": "☁️",
- ":zap": "⚡️",
- ":star": "⭐️",
- ":cookie": "🍪",
- ":pizza": "🍕",
- ":hamburger": "🍔",
- ":fries": "🍟",
- ":cake": "🎂",
- ":cake_part": "🍰",
- ":coffee": "☕️",
- ":banana": "🍌",
- ":sushi": "🍣",
- ":rice_ball": "🍙",
- ":beer": "🍺",
- ":wine": "🍷",
- ":cocktail": "🍸",
- ":tropical": "🍹",
- ":beers": "🍻",
- ":ghost": "👻",
- ":skull": "💀",
- ":et": "👽",
- ":alien": "👽", // alt.
- ":party": "🎉",
- ":trophy": "🏆",
- ":key": "🔑",
- ":pin": "📌",
- ":postal_horn": "📯",
- ":music": "🎵",
- ":trumpet": "🎺",
- ":guitar": "🎸",
- ":run": "🏃",
- ":bike": "🚲",
- ":soccer": "⚽️",
- ":football": "🏈",
- ":8ball": "🎱",
- ":clapper": "🎬",
- ":microphone": "🎤",
- ":cheese": "🧀",
+ ":rainbow": "🌈",
+ ":cloud": "☁️",
+ ":zap": "⚡️",
+ ":star": "⭐️",
+ ":cookie": "🍪",
+ ":pizza": "🍕",
+ ":hamburger": "🍔",
+ ":fries": "🍟",
+ ":cake": "🎂",
+ ":cake_part": "🍰",
+ ":coffee": "☕️",
+ ":banana": "🍌",
+ ":sushi": "🍣",
+ ":rice_ball": "🍙",
+ ":beer": "🍺",
+ ":wine": "🍷",
+ ":cocktail": "🍸",
+ ":tropical": "🍹",
+ ":beers": "🍻",
+ ":ghost": "👻",
+ ":skull": "💀",
+ ":et": "👽",
+ ":alien": "👽", // alt.
+ ":party": "🎉",
+ ":trophy": "🏆",
+ ":key": "🔑",
+ ":pin": "📌",
+ ":postal_horn": "📯",
+ ":music": "🎵",
+ ":trumpet": "🎺",
+ ":guitar": "🎸",
+ ":run": "🏃",
+ ":bike": "🚲",
+ ":soccer": "⚽️",
+ ":football": "🏈",
+ ":8ball": "🎱",
+ ":clapper": "🎬",
+ ":microphone": "🎤",
+ ":cheese": "🧀",
};
// list of emojis in a dictionary, indexed by emoji unicode
diff --git a/addons/mail/static/src/js/emojis_dropdown.js b/addons/mail/static/src/js/emojis_dropdown.js
index bcc7852f102..f2f45f381a8 100644
--- a/addons/mail/static/src/js/emojis_dropdown.js
+++ b/addons/mail/static/src/js/emojis_dropdown.js
@@ -1,19 +1,19 @@
/** @odoo-module **/
-import emojis from '@mail/js/emojis';
+import emojis from "@mail/js/emojis";
-import { Component, useRef, onMounted } from '@odoo/owl';
+import { Component, useRef, onMounted } from "@odoo/owl";
export class EmojisDropdown extends Component {
setup() {
- this.toggleRef = useRef('toggleRef');
+ this.toggleRef = useRef("toggleRef");
this.emojis = emojis;
super.setup();
onMounted(() => {
- new Dropdown(this.toggleRef.el, {
- popperConfig: { placement: 'bottom-end', strategy: 'fixed' },
+ new window.Dropdown(this.toggleRef.el, {
+ popperConfig: { placement: "bottom-end", strategy: "fixed" },
});
});
}
-};
-EmojisDropdown.template = 'mail.EmojisDropdown';
+}
+EmojisDropdown.template = "mail.EmojisDropdown";
diff --git a/addons/mail/static/src/js/emojis_mixin.js b/addons/mail/static/src/js/emojis_mixin.js
index b7d4a466d86..adafdf7cec1 100644
--- a/addons/mail/static/src/js/emojis_mixin.js
+++ b/addons/mail/static/src/js/emojis_mixin.js
@@ -1,7 +1,7 @@
/** @odoo-module **/
-import { escape } from '@web/core/utils/strings';
-import emojis from '@mail/js/emojis';
+import { escape } from "@web/core/utils/strings";
+import emojis from "@mail/js/emojis";
/**
* This mixin gathers a few methods that are used to handle emojis.
@@ -33,9 +33,15 @@ export default {
const textInput = this._getTargetTextElement($(ev.currentTarget));
const selectionStart = textInput.selectionStart;
- textInput.value = textInput.value.slice(0, selectionStart) + unicode + textInput.value.slice(selectionStart);
+ textInput.value =
+ textInput.value.slice(0, selectionStart) +
+ unicode +
+ textInput.value.slice(selectionStart);
textInput.focus();
- textInput.setSelectionRange(selectionStart + unicode.length, selectionStart + unicode.length);
+ textInput.setSelectionRange(
+ selectionStart + unicode.length,
+ selectionStart + unicode.length
+ );
},
//--------------------------------------------------------------------------
@@ -52,7 +58,7 @@ export default {
_formatText(message) {
message = escape(message);
message = this._wrapEmojis(message);
- message = message.replace(/(?:\r\n|\r|\n)/g, '
');
+ message = message.replace(/(?:\r\n|\r|\n)/g, "
");
return message;
},
@@ -66,11 +72,11 @@ export default {
_wrapEmojis(message) {
emojis.forEach(function (emoji) {
message = message.replace(
- new RegExp(emoji.unicode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
- '' + emoji.unicode + ''
+ new RegExp(emoji.unicode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
+ '' + emoji.unicode + ""
);
});
return message;
- }
+ },
};
diff --git a/addons/mail/static/src/js/field_char.js b/addons/mail/static/src/js/field_char.js
index d176eae6e57..2383f2d26bc 100644
--- a/addons/mail/static/src/js/field_char.js
+++ b/addons/mail/static/src/js/field_char.js
@@ -1,10 +1,9 @@
/** @odoo-module **/
import { debounce } from "@web/core/utils/timing";
-import { FieldChar } from 'web.basic_fields';
+import { FieldChar } from "web.basic_fields";
FieldChar.include({
-
//--------------------------------------------------------------------------
// Public
//-------------------------------------------------------------------------
@@ -20,10 +19,12 @@ FieldChar.include({
if (this.nodeOptions.keydown_debounce_delay === undefined) {
this.nodeOptions.keydown_debounce_delay = 2000;
}
- this._triggerOnchange = debounce(this._triggerOnchange, this.nodeOptions.keydown_debounce_delay);
+ this._triggerOnchange = debounce(
+ this._triggerOnchange,
+ this.nodeOptions.keydown_debounce_delay
+ );
},
-
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -55,6 +56,6 @@ FieldChar.include({
* @private
*/
_triggerOnchange: function () {
- this.$input.trigger('change');
+ this.$input.trigger("change");
},
});
diff --git a/addons/mail/static/src/js/m2x_avatar_user.js b/addons/mail/static/src/js/m2x_avatar_user.js
index 557b420a72c..82820d7c76d 100644
--- a/addons/mail/static/src/js/m2x_avatar_user.js
+++ b/addons/mail/static/src/js/m2x_avatar_user.js
@@ -1,12 +1,16 @@
/** @odoo-module **/
-import core from 'web.core';
-import fieldRegistry from 'web.field_registry';
-import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar, Many2OneAvatar } from 'web.relational_fields';
-import session from 'web.session';
-
-import { Component } from '@odoo/owl';
+import core from "web.core";
+import fieldRegistry from "web.field_registry";
+import {
+ FieldMany2ManyTagsAvatar,
+ KanbanMany2ManyTagsAvatar,
+ ListMany2ManyTagsAvatar,
+ Many2OneAvatar,
+} from "web.relational_fields";
+import session from "web.session";
+import { Component } from "@odoo/owl";
// This module defines extensions of the Many2OneAvatar and Many2ManyAvatar
// widgets, which are integrated with the messaging system. They are designed
@@ -23,14 +27,18 @@ import { Component } from '@odoo/owl';
// models than 'res.users'.
const M2XAvatarMixin = {
- supportedModels: ['res.users'],
+ supportedModels: ["res.users"],
init() {
this._super(...arguments);
if (!this.supportedModels.includes(this.field.relation)) {
- throw new Error(`This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(this.supportedModels)}`);
+ throw new Error(
+ `This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(
+ this.supportedModels
+ )}`
+ );
}
- this.className = `${this.className || ''} o_clickable_m2x_avatar`.trim();
+ this.className = `${this.className || ""} o_clickable_m2x_avatar`.trim();
},
//--------------------------------------------------------------------------
@@ -51,14 +59,14 @@ const M2XAvatarMixin = {
export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
events: Object.assign({}, Many2OneAvatar.prototype.events, {
- 'click .o_m2o_avatar > img': '_onAvatarClicked',
+ "click .o_m2o_avatar > img": "_onAvatarClicked",
}),
on_attach_callback() {
- this._registerCommandAssignTo()
+ this._registerCommandAssignTo();
},
- on_detach_callback(){
- this._unregisterCommandAssignTo()
+ on_detach_callback() {
+ this._unregisterCommandAssignTo();
},
//--------------------------------------------------------------------------
@@ -69,24 +77,24 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
* @override
* @private
*/
- _registerCommandAssignTo() {
+ _registerCommandAssignTo() {
const self = this;
if (self.viewType === "form") {
- let provide = async (env, options) => {
+ const provide = async (env, options) => {
if (self.isDestroyed()) {
- return
+ return;
}
- const records = await self._searchAssignTo(options.searchValue, 10)
+ const records = await self._searchAssignTo(options.searchValue, 10);
return records.map((record) => ({
name: record[1],
action: () => {
if (self.isDestroyed()) {
- return
+ return;
}
self.reinitialize({ id: record[0], display_name: record[1] });
- }
- }))
- }
+ },
+ }));
+ };
let getCommandDefinition = (env) => ({
name: env._t("Assign to ..."),
options: {
@@ -106,8 +114,12 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
};
},
});
- core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignTo", getCommandDefinition, self.el);
-
+ core.bus.trigger(
+ "set_legacy_command",
+ "web.Many2OneAvatar.assignTo",
+ getCommandDefinition,
+ self.el
+ );
getCommandDefinition = (env) => ({
name: env._t("Assign/unassign to me"),
@@ -118,19 +130,23 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
},
action() {
if (self.isDestroyed()) {
- return
+ return;
}
if (self.value.res_id === session.user_id[0]) {
self._setValue({
- operation: 'DELETE',
+ operation: "DELETE",
ids: [session.user_id[0]],
- });
+ });
} else {
self.reinitialize({ id: session.user_id[0], display_name: session.name });
}
},
});
- core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition);
+ core.bus.trigger(
+ "set_legacy_command",
+ "web.Many2OneAvatar.assignToMe",
+ getCommandDefinition
+ );
}
},
@@ -152,13 +168,13 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
const domain = this.record.getDomain(this.recordParams);
const context = Object.assign(
this.record.getContext(this.recordParams),
- this.additionalContext,
+ this.additionalContext
);
// Exclude black-listed ids from the domain
const blackListedIds = this._getSearchBlacklist();
if (blackListedIds.length) {
- domain.push(['id', 'not in', blackListedIds]);
+ domain.push(["id", "not in", blackListedIds]);
}
const nameSearch = this._rpc({
@@ -170,7 +186,7 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
operator: "ilike",
limit: limit + 1,
context,
- }
+ },
});
return this.orderer.add(nameSearch);
},
@@ -188,11 +204,11 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
_onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
this._openChat({ userId: this.value.res_id });
- }
+ },
});
export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
- _template: 'mail.KanbanMany2OneAvatarUser',
+ _template: "mail.KanbanMany2OneAvatarUser",
init() {
this._super(...arguments);
@@ -202,7 +218,7 @@ export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
const M2MAvatarMixin = Object.assign(M2XAvatarMixin, {
events: Object.assign({}, FieldMany2ManyTagsAvatar.prototype.events, {
- 'click .o_m2m_avatar': '_onAvatarClicked',
+ "click .o_m2m_avatar": "_onAvatarClicked",
}),
//----------------------------------------------------------------------
@@ -215,18 +231,17 @@ const M2MAvatarMixin = Object.assign(M2XAvatarMixin, {
*/
_onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
- const userId = parseInt(ev.target.getAttribute('data-id'), 10);
+ const userId = parseInt(ev.target.getAttribute("data-id"), 10);
this._openChat({ userId: userId });
},
});
export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, {
-
on_attach_callback() {
- this._registerCommandAssignTo()
+ this._registerCommandAssignTo();
},
- on_detach_callback(){
- this._unregisterCommandAssignTo()
+ on_detach_callback() {
+ this._unregisterCommandAssignTo();
},
//--------------------------------------------------------------------------
@@ -237,15 +252,15 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
* @override
* @private
*/
- _registerCommandAssignTo() {
+ _registerCommandAssignTo() {
const self = this;
if (self.viewType === "form") {
const provide = async (env, options) => {
if (self.isDestroyed()) {
- return
+ return;
}
const many2one = new Many2OneAvatarUser(self, self.name, self.record, {
- mode: 'edit',
+ mode: "edit",
noOpen: true,
noCreate: !self.canCreate,
viewType: self.viewType,
@@ -254,19 +269,19 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
many2one._getSearchBlacklist = function () {
return self.value.res_ids;
};
- const records = await many2one._searchAssignTo(options.searchValue, 10)
+ const records = await many2one._searchAssignTo(options.searchValue, 10);
return records.map((record) => ({
name: record[1],
action: () => {
if (self.isDestroyed()) {
- return
+ return;
}
self._setValue({
- operation: 'ADD_M2M',
- ids: { id: record[0], display_name: record[1] }
+ operation: "ADD_M2M",
+ ids: { id: record[0], display_name: record[1] },
});
},
- }))
+ }));
};
let getCommandDefinition = (env) => ({
name: env._t("Assign to ..."),
@@ -287,7 +302,11 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
};
},
});
- core.bus.trigger("set_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo", getCommandDefinition)
+ core.bus.trigger(
+ "set_legacy_command",
+ "web.FieldMany2ManyTagsAvatar.assignTo",
+ getCommandDefinition
+ );
getCommandDefinition = (env) => ({
name: env._t("Assign/unassign to me"),
@@ -298,22 +317,26 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
},
action() {
if (self.isDestroyed()) {
- return
+ return;
}
if (self.value.res_ids.includes(session.user_id[0])) {
self._setValue({
- operation: 'DELETE',
+ operation: "DELETE",
ids: [session.user_id[0]],
- });
+ });
} else {
self._setValue({
- operation: 'ADD_M2M',
- ids: { id: session.user_id[0], display_name: session.name }
+ operation: "ADD_M2M",
+ ids: { id: session.user_id[0], display_name: session.name },
});
}
},
});
- core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition);
+ core.bus.trigger(
+ "set_legacy_command",
+ "web.Many2OneAvatar.assignToMe",
+ getCommandDefinition
+ );
}
},
@@ -321,19 +344,19 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
* @override
* @private
*/
- _unregisterCommandAssignTo() {
+ _unregisterCommandAssignTo() {
core.bus.trigger("remove_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo");
core.bus.trigger("remove_legacy_command", "web.Many2OneAvatar.assignToMe");
- }
+ },
});
export const KanbanMany2ManyAvatarUser = KanbanMany2ManyTagsAvatar.extend(M2MAvatarMixin, {});
export const ListMany2ManyAvatarUser = ListMany2ManyTagsAvatar.extend(M2MAvatarMixin, {});
-fieldRegistry.add('many2one_avatar_user', Many2OneAvatarUser);
-fieldRegistry.add('kanban.many2one_avatar_user', KanbanMany2OneAvatarUser);
-fieldRegistry.add('activity.many2one_avatar_user', KanbanMany2OneAvatarUser);
-fieldRegistry.add('many2many_avatar_user', Many2ManyAvatarUser);
-fieldRegistry.add('kanban.many2many_avatar_user', KanbanMany2ManyAvatarUser);
-fieldRegistry.add('list.many2many_avatar_user', ListMany2ManyAvatarUser);
+fieldRegistry.add("many2one_avatar_user", Many2OneAvatarUser);
+fieldRegistry.add("kanban.many2one_avatar_user", KanbanMany2OneAvatarUser);
+fieldRegistry.add("activity.many2one_avatar_user", KanbanMany2OneAvatarUser);
+fieldRegistry.add("many2many_avatar_user", Many2ManyAvatarUser);
+fieldRegistry.add("kanban.many2many_avatar_user", KanbanMany2ManyAvatarUser);
+fieldRegistry.add("list.many2many_avatar_user", ListMany2ManyAvatarUser);
diff --git a/addons/mail/static/src/js/tours/mail.js b/addons/mail/static/src/js/tours/mail.js
index 71bcae85235..a7e0b1ba6d7 100644
--- a/addons/mail/static/src/js/tours/mail.js
+++ b/addons/mail/static/src/js/tours/mail.js
@@ -1,59 +1,89 @@
/** @odoo-module **/
-import { _t } from 'web.core';
-import { Markup } from 'web.utils';
-import tour from 'web_tour.tour';
+import { _t } from "web.core";
+import { Markup } from "web.utils";
+import tour from "web_tour.tour";
-tour.register('mail_tour', {
- url: "/web#action=mail.action_discuss",
- sequence: 80,
-}, [{
- trigger: '.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd',
- content: Markup(_t("
Channels make it easy to organize information across different topics and groups.
Try to create your first channel (e.g. sales, marketing, product XYZ, after work party, etc).
")), - position: 'bottom', -}, { - trigger: '.o_DiscussSidebarCategory_addingItemInput', - content: Markup(_t("Create a channel here.
")), - position: 'bottom', - auto: true, - run: function (actions) { - var t = new Date().getTime(); - actions.text("SomeChannel_" + t, this.$anchor); +tour.register( + "mail_tour", + { + url: "/web#action=mail.action_discuss", + sequence: 80, }, -}, { - trigger: ".o_DiscussSidebarCategory_newChannelAutocompleteSuggestions", - content: Markup(_t("Create a public or private channel.
")), - position: 'right', - run() { - this.$consumeEventAnchors.find('li:first').click(); - }, -}, { - trigger: '.o_DiscussView_thread .o_ComposerTextInputView_textarea', - content: Markup(_t("Write a message to the members of the channel here.
You can notify someone with '@' or link another channel with '#'. Start your message with '/' to get the list of possible commands.
")), - position: "top", - width: 350, - run: function (actions) { - var t = new Date().getTime(); - actions.text("SomeText_" + t, this.$anchor); - }, -}, { - trigger: '.o_DiscussView_thread .o_ComposerView_buttonSend', - content: _t("Post your message on the thread"), - position: "top", -}, { - trigger: '.o_DiscussView_thread .o_MessageView', - content: _t("Click on your message"), - position: "top", -}, { - trigger: '.o_DiscussView_thread .o_MessageActionView_actionToggleStar', - content: Markup(_t("Messages can be starred to remind you to check back later.")), - position: "bottom", -}, { - trigger: '.o_DiscussSidebarMailboxView.o-starred-box', - content: _t("Once a message has been starred, you can come back and review it at any time here."), - position: "bottom", -}, { - trigger: '.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_commandAdd', - content: Markup(_t("Chat with coworkers in real-time using direct messages.
You might need to invite users from the Settings app first.
")), - position: 'bottom', -}]); + [ + { + trigger: ".o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd", + content: Markup( + _t( + "Channels make it easy to organize information across different topics and groups.
Try to create your first channel (e.g. sales, marketing, product XYZ, after work party, etc).
" + ) + ), + position: "bottom", + }, + { + trigger: ".o_DiscussSidebarCategory_addingItemInput", + content: Markup(_t("Create a channel here.
")), + position: "bottom", + auto: true, + run: function (actions) { + var t = new Date().getTime(); + actions.text("SomeChannel_" + t, this.$anchor); + }, + }, + { + trigger: ".o_DiscussSidebarCategory_newChannelAutocompleteSuggestions", + content: Markup(_t("Create a public or private channel.
")), + position: "right", + run() { + this.$consumeEventAnchors.find("li:first").click(); + }, + }, + { + trigger: ".o_DiscussView_thread .o_ComposerTextInputView_textarea", + content: Markup( + _t( + "Write a message to the members of the channel here.
You can notify someone with '@' or link another channel with '#'. Start your message with '/' to get the list of possible commands.
" + ) + ), + position: "top", + width: 350, + run: function (actions) { + var t = new Date().getTime(); + actions.text("SomeText_" + t, this.$anchor); + }, + }, + { + trigger: ".o_DiscussView_thread .o_ComposerView_buttonSend", + content: _t("Post your message on the thread"), + position: "top", + }, + { + trigger: ".o_DiscussView_thread .o_MessageView", + content: _t("Click on your message"), + position: "top", + }, + { + trigger: ".o_DiscussView_thread .o_MessageActionView_actionToggleStar", + content: Markup( + _t("Messages can be starred to remind you to check back later.") + ), + position: "bottom", + }, + { + trigger: ".o_DiscussSidebarMailboxView.o-starred-box", + content: _t( + "Once a message has been starred, you can come back and review it at any time here." + ), + position: "bottom", + }, + { + trigger: ".o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_commandAdd", + content: Markup( + _t( + "Chat with coworkers in real-time using direct messages.
You might need to invite users from the Settings app first.
" + ) + ), + position: "bottom", + }, + ] +); diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index b66ae7e8ff3..42d1778884f 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import core from 'web.core'; -import { escape } from '@web/core/utils/strings'; +import core from "web.core"; +import { escape } from "@web/core/utils/strings"; var _t = core._t; @@ -19,12 +19,16 @@ function parseAndTransform(htmlString, transformFunction) { var string = htmlString.replace(/</g, openToken); var children; try { - children = $('' + string + '').contents(); + children = $("
" + string + "") + .contents(); } - return _parseAndTransform(children, transformFunction) - .replace(new RegExp(openToken, "g"), "<"); + return _parseAndTransform(children, transformFunction).replace( + new RegExp(openToken, "g"), + "<" + ); } /** @@ -55,25 +59,26 @@ var urlRegexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\. function linkify(text, attrs) { attrs = attrs || {}; if (attrs.target === undefined) { - attrs.target = '_blank'; + attrs.target = "_blank"; } - if (attrs.target === '_blank') { - attrs.rel = 'noreferrer noopener'; + if (attrs.target === "_blank") { + attrs.rel = "noreferrer noopener"; } attrs = _.map(attrs, function (value, key) { return key + '="' + _.escape(value) + '"'; - }).join(' '); + }).join(" "); return text.replace(urlRegexp, function (url) { - var href = (!/^https?:\/\//i.test(url)) ? "http://" + url : url; - return '' + url + ''; + var href = !/^https?:\/\//i.test(url) ? "http://" + url : url; + return "' + url + ""; }); } function addLink(node, transformChildren) { - if (node.nodeType === 3) { // text node + if (node.nodeType === 3) { + // text node const linkified = linkify(node.data); if (linkified !== node.data) { - const div = document.createElement('div'); + const div = document.createElement("div"); div.innerHTML = linkified; for (const childNode of [...div.childNodes]) { node.parentNode.insertBefore(childNode, node); @@ -83,7 +88,9 @@ function addLink(node, transformChildren) { } return node.textContent; } - if (node.tagName === "A") return node.outerHTML; + if (node.tagName === "A") { + return node.outerHTML; + } transformChildren(); return node.outerHTML; } @@ -94,39 +101,50 @@ function addLink(node, transformChildren) { */ function htmlToTextContentInline(htmlString) { const fragment = document.createDocumentFragment(); - const div = document.createElement('div'); + const div = document.createElement("div"); fragment.appendChild(div); - htmlString = htmlString.replace(/
${htmlString}`;
}
- return div
- .textContent
+ return div.textContent
.trim()
- .replace(/[\n\r]/g, '')
- .replace(/\s\s+/g, ' ');
+ .replace(/[\n\r]/g, "")
+ .replace(/\s\s+/g, " ");
}
function stripHTML(node, transformChildren) {
- if (node.nodeType === 3) return node.data; // text node
- if (node.tagName === "BR") return "\n";
+ if (node.nodeType === 3) {
+ return node.data;
+ } // text node
+ if (node.tagName === "BR") {
+ return "\n";
+ }
return transformChildren();
}
function inline(node, transform_children) {
- if (node.nodeType === 3) return node.data;
- if (node.nodeType === 8) return "";
- if (node.tagName === "BR") return " ";
- if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children();
+ if (node.nodeType === 3) {
+ return node.data;
+ }
+ if (node.nodeType === 8) {
+ return "";
+ }
+ if (node.tagName === "BR") {
+ return " ";
+ }
+ if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) {
+ return transform_children();
+ }
node.innerHTML = transform_children();
return node.outerHTML;
}
// Parses text to find email: Tagada -> [Tagada, address@mail.fr] or False
function parseEmail(text) {
- if (text){
+ if (text) {
var result = text.match(/(.*)<(.*@.*)>/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[2])];
@@ -148,11 +166,11 @@ function parseEmail(text) {
function escapeAndCompactTextContent(content) {
//Removing unwanted extra spaces from message
let value = escape(content).trim();
- value = value.replace(/(\r|\n){2,}/g, 'Test
", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentImage img`).length, + 1, + "attachment should have an image part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentImage_imageOverlay`).length, + 1, + "attachment should have an image overlay part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentImage_aside`).length, + 0, + "attachment should not have an aside element" + ); + }); }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.png", - mimetype: 'image/png', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentImage img`).length, - 1, - "attachment should have an image part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentImage_imageOverlay`).length, - 1, - "attachment should have an image overlay part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentImage_aside`).length, - 0, - "attachment should not have an aside element" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js index 70b59031dd9..8f154837098 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js @@ -1,492 +1,496 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('attachment_list_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("attachment_list_tests.js"); -QUnit.test('simplest layout', async function (assert) { - assert.expect(8); + QUnit.test("simplest layout", async function (assert) { + assert.expect(8); - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_AttachmentList').length, - 1, - "should have attachment list component in DOM" - ); - const attachmentEl = document.querySelector('.o_AttachmentList .o_AttachmentCard'); - assert.strictEqual( - attachmentEl.dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: messageAttachmentId }).localId, - "attachment component should be linked to attachment store model" - ); - assert.strictEqual( - attachmentEl.title, - "test.txt", - "attachment should have filename as title attribute" - ); + assert.strictEqual( + document.querySelectorAll(".o_AttachmentList").length, + 1, + "should have attachment list component in DOM" + ); + const attachmentEl = document.querySelector(".o_AttachmentList .o_AttachmentCard"); + assert.strictEqual( + attachmentEl.dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: messageAttachmentId }) + .localId, + "attachment component should be linked to attachment store model" + ); + assert.strictEqual( + attachmentEl.title, + "test.txt", + "attachment should have filename as title attribute" + ); - assert.strictEqual( - attachmentEl.querySelectorAll(`:scope .o_AttachmentCard_image`).length, - 1, - "attachment should have an image part" - ); - const attachmentImage = document.querySelector(`.o_AttachmentCard_image`); - assert.ok( - attachmentImage.classList.contains('o_image'), - "attachment should have o_image classname (required for mimetype.scss style)" - ); - assert.strictEqual( - attachmentImage.dataset.mimetype, - 'text/plain', - "attachment should have data-mimetype set (required for mimetype.scss style)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentList_details`).length, - 0, - "attachment should not have a details part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentList_aside`).length, - 0, - "attachment should not have an aside part" - ); -}); + assert.strictEqual( + attachmentEl.querySelectorAll(`:scope .o_AttachmentCard_image`).length, + 1, + "attachment should have an image part" + ); + const attachmentImage = document.querySelector(`.o_AttachmentCard_image`); + assert.ok( + attachmentImage.classList.contains("o_image"), + "attachment should have o_image classname (required for mimetype.scss style)" + ); + assert.strictEqual( + attachmentImage.dataset.mimetype, + "text/plain", + "attachment should have data-mimetype set (required for mimetype.scss style)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentList_details`).length, + 0, + "attachment should not have a details part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentList_aside`).length, + 0, + "attachment should not have an aside part" + ); + }); -QUnit.test('simplest layout + editable', async function (assert) { - assert.expect(7); + QUnit.test("simplest layout + editable", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - async mockRPC(route, args) { - if (route.includes('web/image/750')) { - assert.ok( - route.includes('/200x200'), - "should fetch image with 200x200 pixels ratio"); - assert.step('fetch_image'); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + async mockRPC(route, args) { + if (route.includes("web/image/750")) { + assert.ok( + route.includes("/200x200"), + "should fetch image with 200x200 pixels ratio" + ); + assert.step("fetch_image"); + } + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelectorAll(".o_AttachmentList").length, + 1, + "should have attachment component in DOM" + ); + + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_image`).length, + 1, + "attachment should have an image part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_details`).length, + 1, + "attachment should not have a details part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_aside`).length, + 1, + "attachment should have an aside part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_asideItem`).length, + 2, + "attachment should have two aside item" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_asideItemUnlink`).length, + 1, + "attachment should have a delete button" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_asideItemDownload`).length, + 1, + "attachment should have a download button" + ); + }); + + QUnit.test("layout with card details and filename and extension", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_details`).length, + 1, + "attachment should have a details part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_extension`).length, + 1, + "attachment should have its extension shown" + ); + }); + + QUnit.test("view attachment", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.png", + mimetype: "image/png", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_AttachmentImage img", + "attachment should have an image part" + ); + await click(".o_AttachmentImage"); + assert.containsOnce( + document.body, + ".o_Dialog", + "a dialog should have been opened once attachment image is clicked" + ); + assert.containsOnce( + document.body, + ".o_AttachmentViewer", + "an attachment viewer should have been opened once attachment image is clicked" + ); + }); + + QUnit.test("close attachment viewer", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.png", + mimetype: "image/png", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_AttachmentImage img", + "attachment should have an image part" + ); + + await click(".o_AttachmentImage"); + assert.containsOnce( + document.body, + ".o_AttachmentViewer", + "an attachment viewer should have been opened once attachment image is clicked" + ); + + await click(".o_AttachmentViewer_headerItemButtonClose"); + assert.containsNone( + document.body, + ".o_Dialog", + "attachment viewer should be closed after clicking on close button" + ); + }); + + QUnit.test( + "clicking on the delete attachment button multiple times should do the rpc only once", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + async mockRPC(route, args) { + if (route === "/mail/attachment/delete") { + assert.step("attachment_unlink"); + } + }, + }); + await openDiscuss(); + + await click(".o_AttachmentCard_asideItemUnlink"); + + await afterNextRender(() => { + document.querySelector(".o_AttachmentDeleteConfirmView_confirmButton").click(); + document.querySelector(".o_AttachmentDeleteConfirmView_confirmButton").click(); + document.querySelector(".o_AttachmentDeleteConfirmView_confirmButton").click(); + }); + assert.verifySteps(["attachment_unlink"], "The unlink method must be called once"); } - }, - }); - await openDiscuss(); + ); - assert.strictEqual( - document.querySelectorAll('.o_AttachmentList').length, - 1, - "should have attachment component in DOM" - ); + QUnit.test( + "[technical] does not crash when the viewer is closed before image load", + async function (assert) { + /** + * When images are displayed using `src` attribute for the 1st time, it fetches the resource. + * In this case, images are actually displayed (fully fetched and rendered on screen) when + * `Test
", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_details`).length, - 1, - "attachment should have a details part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_extension`).length, - 1, - "attachment should have its extension shown" - ); -}); - -QUnit.test('view attachment', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.png", - mimetype: 'image/png', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage img', - "attachment should have an image part" - ); - await click('.o_AttachmentImage'); - assert.containsOnce( - document.body, - '.o_Dialog', - 'a dialog should have been opened once attachment image is clicked', - ); - assert.containsOnce( - document.body, - '.o_AttachmentViewer', - 'an attachment viewer should have been opened once attachment image is clicked', - ); -}); - -QUnit.test('close attachment viewer', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.png", - mimetype: 'image/png', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage img', - "attachment should have an image part" - ); - - await click('.o_AttachmentImage'); - assert.containsOnce( - document.body, - '.o_AttachmentViewer', - "an attachment viewer should have been opened once attachment image is clicked", - ); - - await click('.o_AttachmentViewer_headerItemButtonClose'); - assert.containsNone( - document.body, - '.o_Dialog', - "attachment viewer should be closed after clicking on close button" - ); -}); - -QUnit.test('clicking on the delete attachment button multiple times should do the rpc only once', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - async mockRPC(route, args) { - if (route === '/mail/attachment/delete') { - assert.step('attachment_unlink'); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.png", + mimetype: "image/png", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + await click(".o_AttachmentImage"); + const imageEl = document.querySelector(".o_AttachmentViewer_viewImage"); + await click(".o_AttachmentViewer_headerItemButtonClose"); + // Simulate image becoming loaded. + let successfulLoad; + try { + imageEl.dispatchEvent(new Event("load", { bubbles: true })); + successfulLoad = true; + } catch { + successfulLoad = false; + } finally { + assert.ok(successfulLoad, "should not crash when the image is loaded"); + } } - }, - }); - await openDiscuss(); + ); - await click('.o_AttachmentCard_asideItemUnlink'); + QUnit.test("plain text file is viewable", async function (assert) { + assert.expect(1); - await afterNextRender(() => { - document.querySelector('.o_AttachmentDeleteConfirmView_confirmButton').click(); - document.querySelector('.o_AttachmentDeleteConfirmView_confirmButton').click(); - document.querySelector('.o_AttachmentDeleteConfirmView_confirmButton').click(); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.hasClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should be viewable" + ); + }); + + QUnit.test("HTML file is viewable", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.html", + mimetype: "text/html", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.hasClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should be viewable" + ); + }); + + QUnit.test("ODT file is not viewable", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.odt", + mimetype: "application/vnd.oasis.opendocument.text", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.doesNotHaveClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should not be viewable" + ); + }); + + QUnit.test("DOCX file is not viewable", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.docx", + mimetype: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "Test
", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.doesNotHaveClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should not be viewable" + ); + }); }); - assert.verifySteps( - ['attachment_unlink'], - "The unlink method must be called once" - ); -}); - -QUnit.test('[technical] does not crash when the viewer is closed before image load', async function (assert) { - /** - * When images are displayed using `src` attribute for the 1st time, it fetches the resource. - * In this case, images are actually displayed (fully fetched and rendered on screen) when - * `Test
", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - await click('.o_AttachmentImage'); - const imageEl = document.querySelector('.o_AttachmentViewer_viewImage'); - await click('.o_AttachmentViewer_headerItemButtonClose'); - // Simulate image becoming loaded. - let successfulLoad; - try { - imageEl.dispatchEvent(new Event('load', { bubbles: true })); - successfulLoad = true; - } catch { - successfulLoad = false; - } finally { - assert.ok(successfulLoad, 'should not crash when the image is loaded'); - } -}); - -QUnit.test('plain text file is viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.hasClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should be viewable", - ); -}); - -QUnit.test('HTML file is viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.html", - mimetype: 'text/html', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.hasClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should be viewable", - ); -}); - -QUnit.test('ODT file is not viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.odt", - mimetype: 'application/vnd.oasis.opendocument.text', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.doesNotHaveClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should not be viewable", - ); -}); - -QUnit.test('DOCX file is not viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.docx", - mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "Test
", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.doesNotHaveClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should not be viewable", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js index 38884a4f881..385d944cc72 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js @@ -1,77 +1,65 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('call_main_view_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("call_main_view_tests.js"); -QUnit.test('Join a call', async function (assert) { - assert.expect(4); + QUnit.test("Join a call", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_callButton"); + + assert.containsOnce(document.body, ".o_CallView", "Should have a call view"); + assert.containsOnce( + document.body, + ".o_CallParticipantCard", + "Should have a call participant card" + ); + assert.containsOnce( + document.body, + ".o_CallMainView_controls", + "Should have call controls" + ); + assert.containsNone( + document.body, + ".o_ThreadViewTopbar_callButton", + "Should not have a join call button anymore" + ); + }); + + QUnit.test("Leave a call", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_callButton"); + assert.containsOnce( + document.body, + ".o_CallActionListView_callToggle", + "Should have a button to leave the call" + ); + await click(".o_CallActionListView_callToggle"); + assert.containsNone(document.body, ".o_CallView", "Should not have a call view"); + }); }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_callButton'); - - assert.containsOnce( - document.body, - '.o_CallView', - "Should have a call view" - ); - assert.containsOnce( - document.body, - '.o_CallParticipantCard', - "Should have a call participant card" - ); - assert.containsOnce( - document.body, - '.o_CallMainView_controls', - "Should have call controls" - ); - assert.containsNone( - document.body, - '.o_ThreadViewTopbar_callButton', - "Should not have a join call button anymore" - ); -}); - -QUnit.test('Leave a call', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_callButton'); - assert.containsOnce( - document.body, - '.o_CallActionListView_callToggle', - "Should have a button to leave the call" - ); - await click('.o_CallActionListView_callToggle'); - assert.containsNone( - document.body, - '.o_CallView', - "Should not have a call view" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js index ca4cb8f7152..181bc5950ab 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js @@ -1,170 +1,175 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; -import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; +import { browser } from "@web/core/browser/browser"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('call_settings_menu_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("call_settings_menu_tests.js"); -QUnit.test('Renders the call settings', async function (assert) { - assert.expect(9); + QUnit.test("Renders the call settings", async function (assert) { + assert.expect(9); - patchWithCleanup(browser, { - navigator: { - ...browser.navigator, - mediaDevices: { - enumerateDevices: () => Promise.resolve([ - { deviceId: 'mockAudioDeviceId', kind: 'audioinput', label: 'mockAudioDeviceLabel' }, - { deviceId: 'mockVideoDeviceId', kind: 'videoinput', label: 'mockVideoDeviceLabel' }, - ]), - }, - } + patchWithCleanup(browser, { + navigator: { + ...browser.navigator, + mediaDevices: { + enumerateDevices: () => + Promise.resolve([ + { + deviceId: "mockAudioDeviceId", + kind: "audioinput", + label: "mockAudioDeviceLabel", + }, + { + deviceId: "mockVideoDeviceId", + kind: "videoinput", + label: "mockVideoDeviceLabel", + }, + ]), + }, + }, + }); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_openCallSettingsButton"); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu", + "Should have a call settings menu" + ); + assert.containsN( + document.body, + ".o_CallSettingsMenu_option", + 5, + "Should have five options" + ); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_optionDeviceSelect", + "should have an audio device selection" + ); + assert.containsOnce( + document.body, + "option[value=mockAudioDeviceId]", + "should have an option to select an audio input device" + ); + assert.containsNone( + document.body, + "option[value=mockVideoDeviceId]", + "should not have an option to select a video input device" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_pushToTalkOption", + "should have an option to toggle push-to-talk" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_voiceThresholdOption", + "should have an option to set the voice detection threshold" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_showOnlyVideoOption", + "should have an option to filter participants who have no video" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_blurOption", + "should have an option to toggle the background blur feature" + ); + }); + + QUnit.test("activate push to talk", async function (assert) { + assert.expect(3); + + patchWithCleanup(browser, { + navigator: { + ...browser.navigator, + mediaDevices: { + enumerateDevices: () => Promise.resolve([]), + }, + }, + }); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_openCallSettingsButton"); + await click(".o_CallSettingsMenu_pushToTalkOption"); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_pushToTalkKeyOption", + "should have an option set the push to talk shortcut key" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_pushToTalkDelayOption", + "should have an option to set the push-to-talk delay" + ); + assert.containsNone( + document.body, + ".o_CallSettingsMenu_voiceThresholdOption", + "should not have an option to set the voice detection threshold" + ); + }); + + QUnit.test("activate blur", async function (assert) { + assert.expect(2); + + patchWithCleanup(browser, { + navigator: { + ...browser.navigator, + mediaDevices: { + enumerateDevices: () => Promise.resolve([]), + }, + }, + }); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_openCallSettingsButton"); + await click(".o_CallSettingsMenu_blurOption"); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_backgroundBlurIntensityOption", + "should have an option set the background blur intensity" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_edgeBlurIntensityOption", + "should have an option to set the edge blur intensity" + ); + }); }); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_openCallSettingsButton'); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu', - "Should have a call settings menu" - ); - assert.containsN( - document.body, - '.o_CallSettingsMenu_option', - 5, - "Should have five options", - ); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_optionDeviceSelect', - "should have an audio device selection", - ); - assert.containsOnce( - document.body, - 'option[value=mockAudioDeviceId]', - "should have an option to select an audio input device", - ); - assert.containsNone( - document.body, - 'option[value=mockVideoDeviceId]', - "should not have an option to select a video input device", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_pushToTalkOption', - "should have an option to toggle push-to-talk", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_voiceThresholdOption', - "should have an option to set the voice detection threshold", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_showOnlyVideoOption', - "should have an option to filter participants who have no video", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_blurOption', - "should have an option to toggle the background blur feature", - ); -}); - -QUnit.test('activate push to talk', async function (assert) { - assert.expect(3); - - patchWithCleanup(browser, { - navigator: { - ...browser.navigator, - mediaDevices: { - enumerateDevices: () => Promise.resolve([]), - }, - } - }); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_openCallSettingsButton'); - await click('.o_CallSettingsMenu_pushToTalkOption'); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_pushToTalkKeyOption', - "should have an option set the push to talk shortcut key", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_pushToTalkDelayOption', - "should have an option to set the push-to-talk delay", - ); - assert.containsNone( - document.body, - '.o_CallSettingsMenu_voiceThresholdOption', - "should not have an option to set the voice detection threshold", - ); -}); - -QUnit.test('activate blur', async function (assert) { - assert.expect(2); - - patchWithCleanup(browser, { - navigator: { - ...browser.navigator, - mediaDevices: { - enumerateDevices: () => Promise.resolve([]), - }, - } - }); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_openCallSettingsButton'); - await click('.o_CallSettingsMenu_blurOption'); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_backgroundBlurIntensityOption', - "should have an option set the background blur intensity", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_edgeBlurIntensityOption', - "should have an option to set the edge blur intensity", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js index abb1bdb3235..764bc2e0825 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js @@ -1,160 +1,167 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('channel_invitation_form_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("channel_invitation_form_tests.js"); -QUnit.test('should display the channel invitation form after clicking on the invite button of a chat', async function (assert) { - assert.expect(1); + QUnit.test( + "should display the channel invitation form after clicking on the invite button of a chat", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await click(`.o_ThreadViewTopbar_inviteButton`); + assert.containsOnce( + document.body, + ".o_ChannelInvitationForm", + "should display the channel invitation form after clicking on the invite button of a chat" + ); + } + ); + + QUnit.test( + "should be able to search for a new user to invite from an existing chat", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const resPartnerId2 = pyEnv["res.partner"].create({ + email: "testpartner2@odoo.com", + name: "TestPartner2", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["res.users"].create({ partner_id: resPartnerId2 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await click(`.o_ThreadViewTopbar_inviteButton`); + await insertText(".o_ChannelInvitationForm_searchInput", "TestPartner2"); + assert.strictEqual( + document.querySelector(`.o_ChannelInvitationFormSelectablePartnerView_name`) + .textContent, + "TestPartner2", + "should display 'TestPartner2' as it matches search term" + ); + } + ); + + QUnit.test( + "should be able to create a new group chat from an existing chat", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const resPartnerId2 = pyEnv["res.partner"].create({ + email: "testpartner2@odoo.com", + name: "TestPartner2", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["res.users"].create({ partner_id: resPartnerId2 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + + await click(`.o_ThreadViewTopbar_inviteButton`); + await insertText(".o_ChannelInvitationForm_searchInput", "TestPartner2"); + await click(`.o_ChannelInvitationFormSelectablePartnerView_checkbox`); + await click(`.o_ChannelInvitationForm_inviteButton`); + assert.strictEqual( + document.querySelector(`.o_ThreadViewTopbar_threadName`).textContent, + "Mitchell Admin, TestPartner, TestPartner2", + "should have created a new group chat with the existing chat members and the selected user" + ); + } + ); + + QUnit.test( + "Invitation form should display channel group restriction", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const resGroupId1 = pyEnv["res.groups"].create({ + name: "testGroup", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [[0, 0, { partner_id: pyEnv.currentPartnerId }]], + channel_type: "channel", + group_public_id: resGroupId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + + await click(`.o_ThreadViewTopbar_inviteButton`); + assert.containsOnce( + document.body, + ".o_ChannelInvitationForm_accessRestrictedToGroup", + "should display the channel restriction warning" + ); + } + ); }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await click(`.o_ThreadViewTopbar_inviteButton`); - assert.containsOnce( - document.body, - '.o_ChannelInvitationForm', - "should display the channel invitation form after clicking on the invite button of a chat" - ); -}); - -QUnit.test('should be able to search for a new user to invite from an existing chat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - const resPartnerId2 = pyEnv['res.partner'].create({ - email: "testpartner2@odoo.com", - name: "TestPartner2", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['res.users'].create({ partner_id: resPartnerId2 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await click(`.o_ThreadViewTopbar_inviteButton`); - await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2"); - assert.strictEqual( - document.querySelector(`.o_ChannelInvitationFormSelectablePartnerView_name`).textContent, - "TestPartner2", - "should display 'TestPartner2' as it matches search term", - ); -}); - -QUnit.test('should be able to create a new group chat from an existing chat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - const resPartnerId2 = pyEnv['res.partner'].create({ - email: "testpartner2@odoo.com", - name: "TestPartner2", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['res.users'].create({ partner_id: resPartnerId2 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - - await click(`.o_ThreadViewTopbar_inviteButton`); - await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2"); - await click(`.o_ChannelInvitationFormSelectablePartnerView_checkbox`); - await click(`.o_ChannelInvitationForm_inviteButton`); - assert.strictEqual( - document.querySelector(`.o_ThreadViewTopbar_threadName`).textContent, - 'Mitchell Admin, TestPartner, TestPartner2', - "should have created a new group chat with the existing chat members and the selected user", - ); -}); - -QUnit.test('Invitation form should display channel group restriction', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const resGroupId1 = pyEnv['res.groups'].create({ - name: "testGroup", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - ], - channel_type: 'channel', - group_public_id: resGroupId1, - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - - await click(`.o_ThreadViewTopbar_inviteButton`); - assert.containsOnce( - document.body, - '.o_ChannelInvitationForm_accessRestrictedToGroup', - "should display the channel restriction warning" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js index 5395a918618..da4e29e5aeb 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js @@ -1,227 +1,237 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('channel_member_list_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("channel_member_list_tests.js"); -QUnit.test('there should be a button to show member list in the thread view topbar initially', async function (assert) { - assert.expect(1); + QUnit.test( + "there should be a button to show member list in the thread view topbar initially", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_ThreadViewTopbar_showMemberListButton", + "there should be a button to show member list in the thread view topbar initially" + ); + } + ); + + QUnit.test( + "should show member list when clicking on show member list button in thread view topbar", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsOnce( + document.body, + ".o_ChannelMemberListView", + "should show member list when clicking on show member list button in thread view topbar" + ); + } + ); + + QUnit.test("should have correct members in member list", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsN( + document.body, + ".o_ChannelMemberView", + 2, + "should have 2 members in member list" + ); + assert.containsOnce( + document.body, + `.o_ChannelMemberView[data-partner-id="${pyEnv.currentPartnerId}"]`, + "should have current partner in member list (current partner is a member)" + ); + assert.containsOnce( + document.body, + `.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`, + "should have 'Demo' in member list ('Demo' is a member)" + ); + }); + + QUnit.test( + "there should be a button to hide member list in the thread view topbar when the member list is visible", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsOnce( + document.body, + ".o_ThreadViewTopbar_hideMemberListButton", + "there should be a button to hide member list in the thread view topbar when the member list is visible" + ); + } + ); + + QUnit.test( + "should show a button to load more members if they are not all loaded", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; + for (let i = 0; i < 101; i++) { + const resPartnerId = pyEnv["res.partner"].create({ name: "name" + i }); + channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); + } + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids, + channel_type: "channel", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsOnce( + document.body, + ".o_ChannelMemberListView_loadMoreButton", + "should have a load more button because 100 members were fetched initially and there are 102 members in total" + ); + } + ); + + QUnit.test("Load more button should load more members", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; + for (let i = 0; i < 101; i++) { + const resPartnerId = pyEnv["res.partner"].create({ name: "name" + i }); + channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); + } + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids, + channel_type: "channel", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + await click(".o_ChannelMemberListView_loadMoreButton"); + assert.containsN( + document.body, + ".o_ChannelMemberView", + 102, + "should load all the members because 100 members were fetched initially, and load more fetched the remaining 2 members" + ); + }); + + QUnit.test( + "chat with member should be opened after clicking on channel member", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "channel", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + + await click(".o_ThreadViewTopbar_showMemberListButton"); + await click(`.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`); + assert.containsOnce( + document.body, + `.o_ThreadView[data-correspondent-id="${resPartnerId1}"]`, + "Chat with member Demo should be opened" + ); + } + ); }); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_ThreadViewTopbar_showMemberListButton', - "there should be a button to show member list in the thread view topbar initially", - ); -}); - -QUnit.test('should show member list when clicking on show member list button in thread view topbar', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsOnce( - document.body, - '.o_ChannelMemberListView', - "should show member list when clicking on show member list button in thread view topbar", - ); -}); - -QUnit.test('should have correct members in member list', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsN( - document.body, - '.o_ChannelMemberView', - 2, - "should have 2 members in member list", - ); - assert.containsOnce( - document.body, - `.o_ChannelMemberView[data-partner-id="${pyEnv.currentPartnerId}"]`, - "should have current partner in member list (current partner is a member)", - ); - assert.containsOnce( - document.body, - `.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`, - "should have 'Demo' in member list ('Demo' is a member)", - ); -}); - -QUnit.test('there should be a button to hide member list in the thread view topbar when the member list is visible', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsOnce( - document.body, - '.o_ThreadViewTopbar_hideMemberListButton', - "there should be a button to hide member list in the thread view topbar when the member list is visible", - ); -}); - -QUnit.test('should show a button to load more members if they are not all loaded', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; - for (let i = 0; i < 101; i++) { - const resPartnerId = pyEnv['res.partner'].create({ name: "name" + i }); - channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); - } - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids, - channel_type: 'channel', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsOnce( - document.body, - '.o_ChannelMemberListView_loadMoreButton', - "should have a load more button because 100 members were fetched initially and there are 102 members in total", - ); -}); - - -QUnit.test('Load more button should load more members', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; - for (let i = 0; i < 101; i++) { - const resPartnerId = pyEnv['res.partner'].create({ name: "name" + i }); - channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); - } - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids, - channel_type: 'channel', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - await click('.o_ChannelMemberListView_loadMoreButton'); - assert.containsN( - document.body, - '.o_ChannelMemberView', - 102, - "should load all the members because 100 members were fetched initially, and load more fetched the remaining 2 members", - ); -}); - -QUnit.test('chat with member should be opened after clicking on channel member', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'channel', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - - await click('.o_ThreadViewTopbar_showMemberListButton'); - await click(`.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`); - assert.containsOnce( - document.body, - `.o_ThreadView[data-correspondent-id="${resPartnerId1}"]`, - "Chat with member Demo should be opened", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js index 1080b6ccafe..80f6ae75099 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js @@ -1,69 +1,78 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('channel_preview_view_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("channel_preview_view_tests.js"); -QUnit.test('mark as read', async function (assert) { - assert.expect(6); + QUnit.test("mark as read", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, // mandatory for good working of test, but ideally should be deduced by other server data - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { - partner_id: resPartnerId1, - }], - ], + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, // mandatory for good working of test, but ideally should be deduced by other server data + partner_id: pyEnv.currentPartnerId, + }, + ], + [ + 0, + 0, + { + partner_id: resPartnerId1, + }, + ], + ], + }); + const [mailMessageId1] = pyEnv["mail.message"].create([ + { author_id: resPartnerId1, model: "mail.channel", res_id: mailChannelId1 }, + { author_id: resPartnerId1, model: "mail.channel", res_id: mailChannelId1 }, + ]); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + + const { click } = await start({ + async mockRPC(route, args) { + if (route.includes("set_last_seen_message")) { + assert.step("set_last_seen_message"); + } + }, + }); + await click(".o_MessagingMenu_toggler"); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_markAsRead", + "should have the mark as read button" + ); + + await click(".o_ChannelPreviewView_markAsRead"); + assert.verifySteps(["set_last_seen_message"], "should have marked the thread as seen"); + assert.hasClass( + document.querySelector(".o_ChannelPreviewView"), + "o-muted", + "should be muted once marked as read" + ); + assert.containsNone( + document.body, + ".o_ChannelPreviewView_markAsRead", + "should no longer have the mark as read button" + ); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have opened the thread" + ); + }); }); - const [mailMessageId1] = pyEnv['mail.message'].create([ - { author_id: resPartnerId1, model: 'mail.channel', res_id: mailChannelId1 }, - { author_id: resPartnerId1, model: 'mail.channel', res_id: mailChannelId1 }, - ]); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - - const { click } = await start({ - async mockRPC(route, args) { - if (route.includes('set_last_seen_message')) { - assert.step('set_last_seen_message'); - } - }, - }); - await click('.o_MessagingMenu_toggler'); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_markAsRead', - "should have the mark as read button" - ); - - await click('.o_ChannelPreviewView_markAsRead'); - assert.verifySteps( - ['set_last_seen_message'], - "should have marked the thread as seen" - ); - assert.hasClass( - document.querySelector('.o_ChannelPreviewView'), - 'o-muted', - "should be muted once marked as read" - ); - assert.containsNone( - document.body, - '.o_ChannelPreviewView_markAsRead', - "should no longer have the mark as read button" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have opened the thread" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js index 820452c47c4..a4214ca4f01 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js @@ -1,1986 +1,2261 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size'; +import { makeDeferred } from "@mail/utils/deferred"; +import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; import { afterNextRender, isScrolledToBottom, nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { file, dom } from 'web.test_utils'; +import { file, dom } from "web.test_utils"; const { createFile, inputFiles } = file; const { triggerEvent } = dom; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chat_window_manager_tests.js'); - -QUnit.test('[technical] messaging not created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(1); - - const messagingBeforeCreationDeferred = makeDeferred(); - const { afterNextRender } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', - }); - - // simulate messaging being created - await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); - - assert.containsOnce( - document.body, - '.o_ChatWindowManager', - "should contain chat window manager after messaging has been created" - ); -}); - -QUnit.test('initial mount', async function (assert) { - assert.expect(1); - - await start(); - assert.containsOnce( - document.body, - '.o_ChatWindowManager', - "should have chat window manager" - ); -}); - -QUnit.test('chat window new message: basic rendering', async function (assert) { - assert.expect(10); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header`).length, - 1, - "should have a header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`).length, - 1, - "should have name part in header" - ); - assert.strictEqual( - document.querySelector(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`).textContent, - "New message", - "should display 'new message' in the header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_command`).length, - 1, - "should have 1 command in header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose`).length, - 1, - "should have command to close chat window" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_newMessageForm`).length, - 1, - "should have a new message chat window container" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_newMessageFormLabel`).length, - 1, - "should have a part in selection with label" - ); - assert.strictEqual( - document.querySelector(`.o_ChatWindow_newMessageFormLabel`).textContent.trim(), - "To:", - "should have label 'To:' in selection" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_newMessageFormInput`).length, - 1, - "should have an input in selection" - ); -}); - -QUnit.test('chat window new message: focused on open [REQUIRE FOCUS]', async function (assert) { - assert.expect(2); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.ok( - document.querySelector(`.o_ChatWindow`).classList.contains('o-focused'), - "chat window should be focused" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ChatWindow_newMessageFormInput`), - "chat window focused = selection input focused" - ); -}); - -QUnit.test('chat window new message: close', async function (assert) { - assert.expect(1); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - await click(`.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 0, - "chat window should be closed" - ); -}); - -QUnit.test('chat window new message: fold', async function (assert) { - assert.expect(6); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.doesNotHaveClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should not be folded by default" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_newMessageForm', - "chat window should have new message form" - ); - - await click(`.o_ChatWindow_header`); - assert.hasClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should become folded" - ); - assert.containsNone( - document.body, - '.o_ChatWindow_newMessageForm', - "chat window should not have new message form" - ); - - await click(`.o_ChatWindow_header`); - assert.doesNotHaveClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should become unfolded" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_newMessageForm', - "chat window should have new message form" - ); -}); - -QUnit.test('open chat from "new message" chat window should open chat in place of this "new message" chat window', async function (assert) { - /** - * InnerWith computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 - */ - assert.expect(12); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Partner 131" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const [, mailChannelId2] = pyEnv['mail.channel'].create([ - { - name: 'channel-1', - channel_member_ids: [ - [0, 0, { - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - name: 'channel-2', - channel_member_ids: [ - [0, 0, { - is_minimized: false, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - ]); - const imSearchDef = makeDeferred(); - patchUiSize({ width: 1920 }); - const { click, insertText } = await start({ - mockRPC(route, args) { - if (args.method === 'im_search') { - imSearchDef.resolve(); - } - } - }); - assert.containsN( - document.body, - '.o_ChatWindow', - 1, - "should have 1 chat window initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow.o-new-message', - "should not have any 'new message' chat window initially" - ); - - // open "new message" chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.containsOnce( - document.body, - '.o_ChatWindow.o-new-message', - "should have 'new message' chat window after clicking 'new message' in messaging menu" - ); - assert.containsN( - document.body, - '.o_ChatWindow', - 2, - "should have 2 chat windows after opening 'new message' chat window", - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_newMessageFormInput', - "'new message' chat window should have new message form input" - ); - assert.hasClass( - document.querySelector('.o_ChatWindow[data-visible-index="1"]'), - 'o-new-message', - "'new message' chat window should be the last chat window initially", - ); - - // open channel-2 - await click(`.o_MessagingMenu_toggler`); - await click(`.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]`); - assert.containsN( - document.body, - '.o_ChatWindow', - 3, - "should have 3 chat windows after opening chat window of 'channel-2'", - ); - assert.hasClass( - document.querySelector('.o_ChatWindow[data-visible-index="1"]'), - 'o-new-message', - "'new message' chat window should be in the middle after opening chat window of 'channel-2'", - ); - - // search for a user in "new message" autocomplete - await insertText('.o_ChatWindow_newMessageFormInput', "131"); - // Wait for search RPC to be resolved. The following await lines are - // necessary because autocomplete is an external lib therefore it is not - // possible to use `afterNextRender`. - await imSearchDef; - await nextAnimationFrame(); - const link = document.querySelector('.ui-autocomplete .ui-menu-item a'); - assert.ok( - link, - "should have autocomplete suggestion after typing on 'new message' input" - ); - assert.strictEqual( - link.textContent, - "Partner 131", - "autocomplete suggestion should target the partner matching search term" - ); - - await afterNextRender(() => link.click()); - assert.containsNone( - document.body, - '.o_ChatWindow.o-new-message', - "should have removed the 'new message' chat window after selecting a partner" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeaderView_name').textContent, - "Partner 131", - "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle" - ); -}); - -QUnit.test('new message chat window should close on selecting the user if chat with the user is already open', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Partner 131" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - name: "Partner 131", - }); - const { afterEvent, click } = await start(); - - // open "new message" chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - - // search for a user in "new message" autocomplete - await afterEvent({ - eventName: 'o-AutocompleteInput-source', - func: () => { - document.execCommand('insertText', false, "131"); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keyup')); - }, - message: "should wait until autocomplete ui sourced its data", - predicate: () => true, - }); - await nextAnimationFrame(); - const link = document.querySelector('.ui-autocomplete .ui-menu-item a'); - - await afterNextRender(() => link.click()); - assert.containsNone( - document.body, - '.o_ChatWindow_newMessageFormInput', - "'new message' chat window should not be there" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have only one chat window after selecting user whose chat is already open", - ); -}); - -QUnit.test('new message autocomplete should automatically select first result', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Partner 131" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const imSearchDef = makeDeferred(); - const { click } = await start({ - mockRPC(route, args) { - if (args.method === 'im_search') { - imSearchDef.resolve(); - } - }, - }); - - // open "new message" chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - - // search for a user in "new message" autocomplete - document.execCommand('insertText', false, "131"); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keyup')); - // Wait for search RPC to be resolved. The following await lines are - // necessary because autocomplete is an external lib therefore it is not - // possible to use `afterNextRender`. - await imSearchDef; - await nextAnimationFrame(); - assert.hasClass( - document.querySelector('.ui-autocomplete .ui-menu-item a'), - 'ui-state-active', - "first autocomplete result should be automatically selected", - ); -}); - -QUnit.test('chat window: basic rendering', async function (assert) { - assert.expect(15); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { click } = await start(); - - await click(`.o_MessagingMenu_toggler`); - await click(`.o_NotificationListView_preview`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - const chatWindow = document.querySelector(`.o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`); - assert.ok( - chatWindow, - 'should have open a chat window of channel' - ); - assert.strictEqual( - chatWindow.querySelectorAll(`:scope .o_ChatWindow_header`).length, - 1, - "should have header part" - ); - const chatWindowHeader = chatWindow.querySelector(`:scope .o_ChatWindow_header`); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ThreadIconView`).length, - 1, - "should have thread icon in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_name`).length, - 1, - "should have thread name in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelector(`:scope .o_ChatWindowHeaderView_name`).textContent, - "General", - "should have correct thread name in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_command`).length, - 6, - "should have 6 commands in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandPhone`).length, - 1, - "should have command to start an RTC call in audio mode" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandCamera`).length, - 1, - "should have command to start an RTC call in video mode" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandShowMemberList`).length, - 1, - "should have command to show the member list" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandShowCallSettingsMenu`).length, - 1, - "should have command to show the call settings menu" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandExpand`).length, - 1, - "should have command to expand thread in discuss" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandClose`).length, - 1, - "should have command to close chat window" - ); - assert.strictEqual( - chatWindow.querySelectorAll(`:scope .o_ChatWindow_thread`).length, - 1, - "should have part to display thread content inside chat window" - ); - assert.ok( - chatWindow.querySelector(`:scope .o_ChatWindow_thread`).classList.contains('o_ThreadView'), - "thread part should use component ThreadView" - ); -}); - -QUnit.test('chat window: fold', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - const { click } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fold') { - assert.step(`rpc:${args.method}/${args.kwargs.state}`); - } - }, - }); - // Open Thread - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window should have a thread" - ); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after opening chat window" - ); - - // Fold chat window - await click(`.o_ChatWindow_header`); - assert.verifySteps( - ['rpc:channel_fold/folded'], - "should sync fold state 'folded' with server after folding chat window" - ); - assert.containsNone( - document.body, - '.o_ChatWindow_thread', - "chat window should not have any thread" - ); - - // Unfold chat window - await click(`.o_ChatWindow_header`); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after unfolding chat window" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window should have a thread" - ); -}); - -QUnit.test('chat window: open / close', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - const { click } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fold') { - assert.step(`rpc:channel_fold/${args.kwargs.state}`); - } - }, - }); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have a chat window initially" - ); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window after clicking on thread preview" - ); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after opening chat window" - ); - - // Close chat window - await click(`.o_ChatWindowHeaderView_commandClose`); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have a chat window after closing it" - ); - assert.verifySteps( - ['rpc:channel_fold/closed'], - "should sync fold state 'closed' with server after closing chat window" - ); - - // Reopen chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window again after clicking on thread preview again" - ); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after opening chat window again" - ); -}); - -QUnit.test('Mobile: opening a chat window should not update channel state on the server', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'closed', - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - patchUiSize({ size: SIZES.SM }); - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window after clicking on thread preview" - ); - const [member] = pyEnv['mail.channel.member'].searchRead([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - assert.strictEqual( - member.fold_state, - 'closed', - 'opening a chat window in mobile should not update channel state on the server', - ); -}); - -QUnit.test('Mobile: closing a chat window should not update channel state on the server', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - patchUiSize({ size: SIZES.SM }); - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window after clicking on thread preview" - ); - // Close chat window - await click(`.o_ChatWindowHeaderView_commandClose`); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have a chat window after closing it" - ); - const [member] = pyEnv['mail.channel.member'].searchRead([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - assert.strictEqual( - member.fold_state, - 'open', - 'closing the chat window should not update channel state on the server', - ); -}); - -QUnit.test("Mobile: chat window shouldn't open automatically after receiving a new message", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['mail.channel'].records = [ - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - id: resPartnerId1, - uuid: 'channel-10-uuid', - }, - ]; - patchUiSize({ size: SIZES.SM }); - const { messaging } = await start(); - - // simulate receiving a message - messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - }); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_ChatWindow', - "On mobile, the chat window shouldn't open automatically after receiving a new message" - ); -}); - -QUnit.test('chat window: close on ESCAPE', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - pyEnv['res.partner'].create({ name: "TestPartner" }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - const { click, insertText } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fold') { - assert.step(`rpc:channel_fold/${args.kwargs.state}`); - } - }, - }); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "chat window should be opened initially" - ); - - await click(`.o_ComposerView_buttonEmojis`); - assert.containsOnce( - document.body, - '.o_EmojiPickerView', - "emoji list should be opened after click on emojis button" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_EmojiPickerView', - "emoji list should be closed after pressing escape on emojis button" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "chat window should still be opened after pressing escape on emojis button" - ); - - await insertText('.o_ComposerTextInputView_textarea', "@"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display mention suggestions on typing '@'" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "mention suggestion should be closed after pressing escape on mention suggestion" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "chat window should still be opened after pressing escape on mention suggestion" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ChatWindow', - "chat window should be closed after pressing escape if there was no other priority escape handler" - ); - assert.verifySteps(['rpc:channel_fold/closed']); -}); - -QUnit.test('focus next visible chat window when closing current chat window with ESCAPE [REQUIRE FOCUS]', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 2 visible chat windows: - * 10 + 340 + 5 + 340 + 10 = 705 < 1920 - */ - assert.expect(4); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - ]); - patchUiSize({ width: 1920 }); - await start(); - assert.containsN( - document.body, - '.o_ChatWindow .o_ComposerTextInputView_textarea', - 2, - "2 chat windows should be present initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow.o-folded', - "both chat windows should be open" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(ev); - }); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "only one chat window should remain after pressing escape on first chat window" - ); - assert.hasClass( - document.querySelector('.o_ChatWindow'), - 'o-focused', - "next visible chat window should be focused after pressing escape on first chat window" - ); -}); - -QUnit.test('chat window: composer state conservation on toggle discuss', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, insertText, messaging, openDiscuss, openView } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - // Set content of the composer of the chat window - await insertText('.o_ComposerTextInputView_textarea', 'XDU for the win !'); - assert.containsNone( - document.body, - '.o_ComposerView .o_AttachmentCard', - "composer should have no attachment initially" - ); - // Set attachments of the composer - const files = [ - await createFile({ - name: 'text state conservation on toggle home menu.txt', - content: 'hello, world', - contentType: 'text/plain', - }), - await createFile({ - name: 'text2 state conservation on toggle home menu.txt', - content: 'hello, xdu is da best man', - contentType: 'text/plain', - }) - ]; - await afterNextRender(() => - inputFiles( - messaging.chatWindowManager.chatWindows[0].threadView.composerView.fileUploader.fileInput, - files - ) - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "XDU for the win !", - "chat window composer initial text input should contain 'XDU for the win !'" - ); - assert.containsN( - document.body, - '.o_ComposerView .o_AttachmentCard', - 2, - "composer should have 2 total attachments after adding 2 attachments" - ); - - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsNone(document.body, '.o_ChatWindow', "should not have any chat window after opening discuss"); - - await openView({ - res_id: mailChannelId, - res_model: 'mail.channel', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "XDU for the win !", - "chat window composer should still have the same input after closing discuss" - ); - assert.containsN( - document.body, - '.o_ComposerView .o_AttachmentCard', - 2, - "Chat window composer should have 2 attachments after closing discuss" - ); -}); - -QUnit.test('chat window: scroll conservation on toggle discuss', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 100; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chat_window_manager_tests.js"); + + QUnit.test("[technical] messaging not created", async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(1); + + const messagingBeforeCreationDeferred = makeDeferred(); + const { afterNextRender } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + + // simulate messaging being created + await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); + + assert.containsOnce( + document.body, + ".o_ChatWindowManager", + "should contain chat window manager after messaging has been created" + ); }); - } - const { afterEvent, click, openDiscuss, openView } = await start(); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_NotificationListView_preview').click(), - message: "should wait until channel scrolled to its last message after opening it from the messaging menu", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) + + QUnit.test("initial mount", async function (assert) { + assert.expect(1); + + await start(); + assert.containsOnce( + document.body, + ".o_ChatWindowManager", + "should have chat window manager" ); - }, - }); - // Set a scroll position to chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; - }, - message: "should wait until channel scrolled to 142 after setting this value manually", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 + }); + + QUnit.test("chat window new message: basic rendering", async function (assert) { + assert.expect(10); + + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" ); - }, - }); - - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsNone(document.body, '.o_ChatWindow', "should not have any chat window after opening discuss"); - - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => openView({ - res_id: mailChannelId1, - res_model: 'mail.channel', - views: [[false, 'list']], - }), - message: "should wait until channel restored its scroll to 142 after closing discuss", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header`).length, + 1, + "should have a header" ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "chat window scrollTop should still be the same after closing discuss" - ); -}); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`) + .length, + 1, + "should have name part in header" + ); + assert.strictEqual( + document.querySelector(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`) + .textContent, + "New message", + "should display 'new message' in the header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_command`) + .length, + 1, + "should have 1 command in header" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose` + ).length, + 1, + "should have command to close chat window" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageForm`).length, + 1, + "should have a new message chat window container" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageFormLabel`).length, + 1, + "should have a part in selection with label" + ); + assert.strictEqual( + document.querySelector(`.o_ChatWindow_newMessageFormLabel`).textContent.trim(), + "To:", + "should have label 'To:' in selection" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageFormInput`).length, + 1, + "should have an input in selection" + ); + }); -QUnit.test('open 2 different chat windows: enough screen width [REQUIRE FOCUS]', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 2 visible chat windows: - * 10 + 340 + 5 + 340 + 10 = 705 < 1920 - */ - assert.expect(8); + QUnit.test( + "chat window new message: focused on open [REQUIRE FOCUS]", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]); - patchUiSize({ width: 1920 }); // enough to fit at least 2 chat windows - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(` + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.ok( + document.querySelector(`.o_ChatWindow`).classList.contains("o-focused"), + "chat window should be focused" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ChatWindow_newMessageFormInput`), + "chat window focused = selection input focused" + ); + } + ); + + QUnit.test("chat window new message: close", async function (assert) { + assert.expect(1); + + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + await click(`.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 0, + "chat window should be closed" + ); + }); + + QUnit.test("chat window new message: fold", async function (assert) { + assert.expect(6); + + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.doesNotHaveClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should not be folded by default" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_newMessageForm", + "chat window should have new message form" + ); + + await click(`.o_ChatWindow_header`); + assert.hasClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should become folded" + ); + assert.containsNone( + document.body, + ".o_ChatWindow_newMessageForm", + "chat window should not have new message form" + ); + + await click(`.o_ChatWindow_header`); + assert.doesNotHaveClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should become unfolded" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_newMessageForm", + "chat window should have new message form" + ); + }); + + QUnit.test( + 'open chat from "new message" chat window should open chat in place of this "new message" chat window', + async function (assert) { + /** + * InnerWith computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 + */ + assert.expect(12); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Partner 131" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const [, mailChannelId2] = pyEnv["mail.channel"].create([ + { + name: "channel-1", + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + name: "channel-2", + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + ]); + const imSearchDef = makeDeferred(); + patchUiSize({ width: 1920 }); + const { click, insertText } = await start({ + mockRPC(route, args) { + if (args.method === "im_search") { + imSearchDef.resolve(); + } + }, + }); + assert.containsN( + document.body, + ".o_ChatWindow", + 1, + "should have 1 chat window initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow.o-new-message", + "should not have any 'new message' chat window initially" + ); + + // open "new message" chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.containsOnce( + document.body, + ".o_ChatWindow.o-new-message", + "should have 'new message' chat window after clicking 'new message' in messaging menu" + ); + assert.containsN( + document.body, + ".o_ChatWindow", + 2, + "should have 2 chat windows after opening 'new message' chat window" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_newMessageFormInput", + "'new message' chat window should have new message form input" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="1"]'), + "o-new-message", + "'new message' chat window should be the last chat window initially" + ); + + // open channel-2 + await click(`.o_MessagingMenu_toggler`); + await click(`.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]`); + assert.containsN( + document.body, + ".o_ChatWindow", + 3, + "should have 3 chat windows after opening chat window of 'channel-2'" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="1"]'), + "o-new-message", + "'new message' chat window should be in the middle after opening chat window of 'channel-2'" + ); + + // search for a user in "new message" autocomplete + await insertText(".o_ChatWindow_newMessageFormInput", "131"); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + const link = document.querySelector(".ui-autocomplete .ui-menu-item a"); + assert.ok( + link, + "should have autocomplete suggestion after typing on 'new message' input" + ); + assert.strictEqual( + link.textContent, + "Partner 131", + "autocomplete suggestion should target the partner matching search term" + ); + + await afterNextRender(() => link.click()); + assert.containsNone( + document.body, + ".o_ChatWindow.o-new-message", + "should have removed the 'new message' chat window after selecting a partner" + ); + assert.strictEqual( + document.querySelector( + '.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeaderView_name' + ).textContent, + "Partner 131", + "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle" + ); + } + ); + + QUnit.test( + "new message chat window should close on selecting the user if chat with the user is already open", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Partner 131" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + name: "Partner 131", + }); + const { afterEvent, click } = await start(); + + // open "new message" chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + + // search for a user in "new message" autocomplete + await afterEvent({ + eventName: "o-AutocompleteInput-source", + func: () => { + document.execCommand("insertText", false, "131"); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + }, + message: "should wait until autocomplete ui sourced its data", + predicate: () => true, + }); + await nextAnimationFrame(); + const link = document.querySelector(".ui-autocomplete .ui-menu-item a"); + + await afterNextRender(() => link.click()); + assert.containsNone( + document.body, + ".o_ChatWindow_newMessageFormInput", + "'new message' chat window should not be there" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have only one chat window after selecting user whose chat is already open" + ); + } + ); + + QUnit.test( + "new message autocomplete should automatically select first result", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Partner 131" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const imSearchDef = makeDeferred(); + const { click } = await start({ + mockRPC(route, args) { + if (args.method === "im_search") { + imSearchDef.resolve(); + } + }, + }); + + // open "new message" chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + + // search for a user in "new message" autocomplete + document.execCommand("insertText", false, "131"); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + assert.hasClass( + document.querySelector(".ui-autocomplete .ui-menu-item a"), + "ui-state-active", + "first autocomplete result should be automatically selected" + ); + } + ); + + QUnit.test("chat window: basic rendering", async function (assert) { + assert.expect(15); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { click } = await start(); + + await click(`.o_MessagingMenu_toggler`); + await click(`.o_NotificationListView_preview`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + const chatWindow = document.querySelector( + `.o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]` + ); + assert.ok(chatWindow, "should have open a chat window of channel"); + assert.strictEqual( + chatWindow.querySelectorAll(`:scope .o_ChatWindow_header`).length, + 1, + "should have header part" + ); + const chatWindowHeader = chatWindow.querySelector(`:scope .o_ChatWindow_header`); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ThreadIconView`).length, + 1, + "should have thread icon in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_name`).length, + 1, + "should have thread name in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelector(`:scope .o_ChatWindowHeaderView_name`).textContent, + "General", + "should have correct thread name in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_command`).length, + 6, + "should have 6 commands in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandPhone`) + .length, + 1, + "should have command to start an RTC call in audio mode" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandCamera`) + .length, + 1, + "should have command to start an RTC call in video mode" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll( + `:scope .o_ChatWindowHeaderView_commandShowMemberList` + ).length, + 1, + "should have command to show the member list" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll( + `:scope .o_ChatWindowHeaderView_commandShowCallSettingsMenu` + ).length, + 1, + "should have command to show the call settings menu" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandExpand`) + .length, + 1, + "should have command to expand thread in discuss" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandClose`) + .length, + 1, + "should have command to close chat window" + ); + assert.strictEqual( + chatWindow.querySelectorAll(`:scope .o_ChatWindow_thread`).length, + 1, + "should have part to display thread content inside chat window" + ); + assert.ok( + chatWindow + .querySelector(`:scope .o_ChatWindow_thread`) + .classList.contains("o_ThreadView"), + "thread part should use component ThreadView" + ); + }); + + QUnit.test("chat window: fold", async function (assert) { + assert.expect(9); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + const { click } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fold") { + assert.step(`rpc:${args.method}/${args.kwargs.state}`); + } + }, + }); + // Open Thread + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window should have a thread" + ); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after opening chat window" + ); + + // Fold chat window + await click(`.o_ChatWindow_header`); + assert.verifySteps( + ["rpc:channel_fold/folded"], + "should sync fold state 'folded' with server after folding chat window" + ); + assert.containsNone( + document.body, + ".o_ChatWindow_thread", + "chat window should not have any thread" + ); + + // Unfold chat window + await click(`.o_ChatWindow_header`); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after unfolding chat window" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window should have a thread" + ); + }); + + QUnit.test("chat window: open / close", async function (assert) { + assert.expect(10); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + const { click } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fold") { + assert.step(`rpc:channel_fold/${args.kwargs.state}`); + } + }, + }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have a chat window initially" + ); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window after clicking on thread preview" + ); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after opening chat window" + ); + + // Close chat window + await click(`.o_ChatWindowHeaderView_commandClose`); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have a chat window after closing it" + ); + assert.verifySteps( + ["rpc:channel_fold/closed"], + "should sync fold state 'closed' with server after closing chat window" + ); + + // Reopen chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window again after clicking on thread preview again" + ); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after opening chat window again" + ); + }); + + QUnit.test( + "Mobile: opening a chat window should not update channel state on the server", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "closed", + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + patchUiSize({ size: SIZES.SM }); + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window after clicking on thread preview" + ); + const [member] = pyEnv["mail.channel.member"].searchRead([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + assert.strictEqual( + member.fold_state, + "closed", + "opening a chat window in mobile should not update channel state on the server" + ); + } + ); + + QUnit.test( + "Mobile: closing a chat window should not update channel state on the server", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + patchUiSize({ size: SIZES.SM }); + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window after clicking on thread preview" + ); + // Close chat window + await click(`.o_ChatWindowHeaderView_commandClose`); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have a chat window after closing it" + ); + const [member] = pyEnv["mail.channel.member"].searchRead([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + assert.strictEqual( + member.fold_state, + "open", + "closing the chat window should not update channel state on the server" + ); + } + ); + + QUnit.test( + "Mobile: chat window shouldn't open automatically after receiving a new message", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["mail.channel"].records = [ + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + id: resPartnerId1, + uuid: "channel-10-uuid", + }, + ]; + patchUiSize({ size: SIZES.SM }); + const { messaging } = await start(); + + // simulate receiving a message + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_ChatWindow", + "On mobile, the chat window shouldn't open automatically after receiving a new message" + ); + } + ); + + QUnit.test("chat window: close on ESCAPE", async function (assert) { + assert.expect(10); + + const pyEnv = await startServer(); + pyEnv["res.partner"].create({ name: "TestPartner" }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + const { click, insertText } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fold") { + assert.step(`rpc:channel_fold/${args.kwargs.state}`); + } + }, + }); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "chat window should be opened initially" + ); + + await click(`.o_ComposerView_buttonEmojis`); + assert.containsOnce( + document.body, + ".o_EmojiPickerView", + "emoji list should be opened after click on emojis button" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_EmojiPickerView", + "emoji list should be closed after pressing escape on emojis button" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "chat window should still be opened after pressing escape on emojis button" + ); + + await insertText(".o_ComposerTextInputView_textarea", "@"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display mention suggestions on typing '@'" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "chat window should still be opened after pressing escape on mention suggestion" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "chat window should be closed after pressing escape if there was no other priority escape handler" + ); + assert.verifySteps(["rpc:channel_fold/closed"]); + }); + + QUnit.test( + "focus next visible chat window when closing current chat window with ESCAPE [REQUIRE FOCUS]", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 2 visible chat windows: + * 10 + 340 + 5 + 340 + 10 = 705 < 1920 + */ + assert.expect(4); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + ]); + patchUiSize({ width: 1920 }); + await start(); + assert.containsN( + document.body, + ".o_ChatWindow .o_ComposerTextInputView_textarea", + 2, + "2 chat windows should be present initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow.o-folded", + "both chat windows should be open" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { + bubbles: true, + key: "Escape", + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(ev); + }); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "only one chat window should remain after pressing escape on first chat window" + ); + assert.hasClass( + document.querySelector(".o_ChatWindow"), + "o-focused", + "next visible chat window should be focused after pressing escape on first chat window" + ); + } + ); + + QUnit.test( + "chat window: composer state conservation on toggle discuss", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, insertText, messaging, openDiscuss, openView } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + // Set content of the composer of the chat window + await insertText(".o_ComposerTextInputView_textarea", "XDU for the win !"); + assert.containsNone( + document.body, + ".o_ComposerView .o_AttachmentCard", + "composer should have no attachment initially" + ); + // Set attachments of the composer + const files = [ + await createFile({ + name: "text state conservation on toggle home menu.txt", + content: "hello, world", + contentType: "text/plain", + }), + await createFile({ + name: "text2 state conservation on toggle home menu.txt", + content: "hello, xdu is da best man", + contentType: "text/plain", + }), + ]; + await afterNextRender(() => + inputFiles( + messaging.chatWindowManager.chatWindows[0].threadView.composerView + .fileUploader.fileInput, + files + ) + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "XDU for the win !", + "chat window composer initial text input should contain 'XDU for the win !'" + ); + assert.containsN( + document.body, + ".o_ComposerView .o_AttachmentCard", + 2, + "composer should have 2 total attachments after adding 2 attachments" + ); + + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have any chat window after opening discuss" + ); + + await openView({ + res_id: mailChannelId, + res_model: "mail.channel", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "XDU for the win !", + "chat window composer should still have the same input after closing discuss" + ); + assert.containsN( + document.body, + ".o_ComposerView .o_AttachmentCard", + 2, + "Chat window composer should have 2 attachments after closing discuss" + ); + } + ); + + QUnit.test("chat window: scroll conservation on toggle discuss", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 100; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, openDiscuss, openView } = await start(); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_NotificationListView_preview").click(), + message: + "should wait until channel scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + // Set a scroll position to chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; + }, + message: + "should wait until channel scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have any chat window after opening discuss" + ); + + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + openView({ + res_id: mailChannelId1, + res_model: "mail.channel", + views: [[false, "list"]], + }), + message: + "should wait until channel restored its scroll to 142 after closing discuss", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after closing discuss" + ); + }); + + QUnit.test( + "open 2 different chat windows: enough screen width [REQUIRE FOCUS]", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 2 visible chat windows: + * 10 + 340 + 5 + 340 + 10 = 705 < 1920 + */ + assert.expect(8); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + ]); + patchUiSize({ width: 1920 }); // enough to fit at least 2 chat windows + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of chat should be open" - ); - assert.ok( - document.querySelector(` + 1, + "chat window of chat should be open" + ); + assert.ok( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of chat should have focus" - ); + ` + ) + .classList.contains("o-focused"), + "chat window of chat should have focus" + ); - await click(`.o_MessagingMenu_toggler`); - await click(` + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 2, - "should have open a new chat window" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open a new chat window" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId2}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of channel should be open" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "chat window of channel should be open" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of chat should still be open" - ); - assert.ok( - document.querySelector(` + 1, + "chat window of chat should still be open" + ); + assert.ok( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId2}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of channel should have focus" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-focused"), + "chat window of channel should have focus" + ); + assert.notOk( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of chat should no longer have focus" - ); -}); + ` + ) + .classList.contains("o-focused"), + "chat window of chat should no longer have focus" + ); + } + ); -QUnit.test('open 3 different chat windows: not enough screen width', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: 900px) - * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1080px - * - * Enough space for 2 visible chat windows, and one hidden chat window: - * 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 900 - * 2 visible chat windows + hidden menu: - * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 - */ - assert.expect(12); + QUnit.test( + "open 3 different chat windows: not enough screen width", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1080px + * + * Enough space for 2 visible chat windows, and one hidden chat window: + * 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 900 + * 2 visible chat windows + hidden menu: + * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 + */ + assert.expect(12); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([ - { name: 'mailChannel1' }, - { name: 'mailChannel2' }, - { name: 'mailChannel3' }, - ]); - patchUiSize({ width: 900 }); // enough to fit 2 chat windows but not 3 - const { click } = await start(); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv[ + "mail.channel" + ].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + { name: "mailChannel3" }, + ]); + patchUiSize({ width: 900 }); // enough to fit 2 chat windows but not 3 + const { click } = await start(); - // open, from systray menu, chat windows of channels with Id 1, 2, then 3 - await click(`.o_MessagingMenu_toggler`); - await click(` + // open, from systray menu, chat windows of channels with Id 1, 2, then 3 + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open 1 visible chat window" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, - 0, - "should not have hidden menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "messaging menu should be hidden" - ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open 1 visible chat window" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 0, + "should not have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); - await click(`.o_MessagingMenu_toggler`); - await click(` + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 2, - "should have open 2 visible chat windows" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, - 0, - "should not have hidden menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "messaging menu should be hidden" - ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open 2 visible chat windows" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 0, + "should not have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); - await click(`.o_MessagingMenu_toggler`); - await click(` + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId3}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 2, - "should have open 2 visible chat windows" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, - 1, - "should have hidden menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "messaging menu should be hidden" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open 2 visible chat windows" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 1, + "should have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of channel 1 should be open" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "chat window of channel 1 should be open" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of channel 3 should be open" - ); - assert.ok( - document.querySelector(` + 1, + "chat window of channel 3 should be open" + ); + assert.ok( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of channel 3 should have focus" - ); -}); - -QUnit.test('chat window: switch on TAB', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'channel1' }, { name: 'channel2' }]); - const { click } = await start(); - - await click(`.o_MessagingMenu_toggler`); - await click(` - .o_MessagingMenu_dropdownMenu - .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]` - ); - - assert.containsOnce(document.body, '.o_ChatWindow', "Only 1 chatWindow must be opened"); - const chatWindow = document.querySelector('.o_ChatWindow'); - assert.strictEqual( - chatWindow.querySelector('.o_ChatWindowHeaderView_name').textContent, - 'channel1', - "The name of the only chatWindow should be 'channel1' (channel with ID 1)" - ); - assert.strictEqual( - chatWindow.querySelector('.o_ComposerTextInputView_textarea'), - document.activeElement, - "The chatWindow composer must have focus" - ); - - await afterNextRender(() => - triggerEvent( - chatWindow.querySelector('.o_ChatWindow .o_ComposerTextInputView_textarea'), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - chatWindow.querySelector('.o_ChatWindow .o_ComposerTextInputView_textarea'), - document.activeElement, - "The chatWindow composer still has focus" - ); - - await click(`.o_MessagingMenu_toggler`); - await click(` - .o_MessagingMenu_dropdownMenu - .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]` - ); - - assert.containsN(document.body, '.o_ChatWindow', 2, "2 chatWindows must be opened"); - const chatWindows = document.querySelectorAll('.o_ChatWindow'); - assert.strictEqual( - chatWindows[0].querySelector('.o_ChatWindowHeaderView_name').textContent, - 'channel1', - "The name of the 1st chatWindow should be 'channel1' (channel with ID 1)" - ); - assert.strictEqual( - chatWindows[1].querySelector('.o_ChatWindowHeaderView_name').textContent, - 'channel2', - "The name of the 2nd chatWindow should be 'channel2' (channel with ID 2)" - ); - assert.strictEqual( - chatWindows[1].querySelector('.o_ComposerTextInputView_textarea'), - document.activeElement, - "The 2nd chatWindow composer must have focus (channel with ID 2)" - ); - - await afterNextRender(() => - triggerEvent( - chatWindows[1].querySelector('.o_ComposerTextInputView_textarea'), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.containsN(document.body, '.o_ChatWindow', 2, "2 chatWindows should still be opened"); - assert.strictEqual( - chatWindows[0].querySelector('.o_ComposerTextInputView_textarea'), - document.activeElement, - "The 1st chatWindow composer must have focus (channel with ID 1)" - ); -}); - -QUnit.test('chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]', async function (assert) { - /** - * InnerWith computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 - */ - assert.expect(6); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - ]); - patchUiSize({ width: 1920 }); - await start(); - assert.containsN( - document.body, - '.o_ChatWindow .o_ComposerTextInputView_textarea', - 3, - "initialy, 3 chat windows should be present" - ); - assert.containsNone( - document.body, - '.o_ChatWindow.o-folded', - "all 3 chat windows should be open" - ); - - await afterNextRender(() => { - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea").focus(); - }); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "The chatWindow with visible-index 2 should have the focus" - ); - - await afterNextRender(() => - triggerEvent( - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea"), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "after pressing tab on the chatWindow with visible-index 2, the chatWindow with visible-index 1 should have focus" - ); - - await afterNextRender(() => - triggerEvent( - document.querySelector(".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea"), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "after pressing tab on the chat window with visible-index 1, the chatWindow with visible-index 0 should have focus" - ); - - await afterNextRender(() => - triggerEvent( - document.querySelector(".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea"), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "the chatWindow with visible-index 2 should have the focus after pressing tab on the chatWindow with visible-index 0" - ); -}); - -QUnit.test('chat window with a thread: keep scroll position in message list on folded', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 100; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { afterEvent, click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_NotificationListView_preview').click(), - message: "should wait until channel scrolled to its last message after opening it from the messaging menu", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - // Set a scroll position to chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; - }, - message: "should wait until channel scrolled to 142 after setting this value manually", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "verify chat window initial scrollTop" - ); - - // fold chat window - await click('.o_ChatWindow_header'); - assert.containsNone( - document.body, - ".o_ThreadView", - "chat window should be folded so no ThreadView should be present" - ); - - // unfold chat window - await afterNextRender(() => afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_ChatWindow_header').click(), - message: "should wait until channel restored its scroll position to 142", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - })); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "chat window scrollTop should still be the same when chat window is unfolded" - ); -}); - -QUnit.test('chat window should scroll to the newly posted message just after posting it', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - for (let i = 0; i < 10; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { insertText } = await start(); - - // Set content of the composer of the chat window - await insertText('.o_ComposerTextInputView_textarea', 'WOLOLO'); - // Send a new message in the chatwindow to trigger the scroll - await afterNextRender(() => - triggerEvent( - document.querySelector('.o_ChatWindow .o_ComposerTextInputView_textarea'), - 'keydown', - { key: 'Enter' }, - ) - ); - const messageList = document.querySelector('.o_MessageListView'); - assert.ok( - isScrolledToBottom(messageList), - "chat window should scroll to the newly posted message just after posting it" - ); -}); - -QUnit.test('chat window: post message on non-mailing channel with "CTRL-Enter" keyboard shortcut for small screen size', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - patchUiSize({ size: SIZES.SM }); - const { click, insertText } = await start(); - - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { ctrlKey: true, key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in channel after posting message from pressing 'CTRL-Enter' in text input of composer for small screen" - ); -}); - -QUnit.test('chat window with a thread: keep scroll position in message list on toggle discuss when folded', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 100; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { afterEvent, click, openDiscuss, openView } = await start(); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_NotificationListView_preview').click(), - message: "should wait until channel scrolled to its last message after opening it from the messaging menu", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - // Set a scroll position to chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142, - message: "should wait until channel scrolled to 142 after setting this value manually", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - }); - // fold chat window - await click('.o_ChatWindow_header'); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsNone(document.body, '.o_ChatWindow', "should not have any chat window after opening discuss"); - - await openView({ - res_id: mailChannelId1, - res_model: 'mail.channel', - views: [[false, 'list']], - }); - // unfold chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_ChatWindow_header').click(), - message: "should wait until channel restored its scroll position to the last saved value (142)", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "chat window scrollTop should still be the same after closing discuss" - ); -}); - -QUnit.test('chat window does not fetch messages if hidden', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: 900px) - * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1080px - * - * Enough space for 2 visible chat windows, and one hidden chat window: - * 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 - * 2 visible chat windows + hidden menu: - * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 - */ - assert.expect(11); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }] - ], - name: "Channel #10", - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: "Channel #11", - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: "Channel #12", - }, - ]); - patchUiSize({ width: 900 }); - const { click } = await start({ - mockRPC(route, args) { - if (route === '/mail/channel/messages') { - const { channel_id } = args; - assert.step(`rpc:/mail/channel/messages:${channel_id}`); + ` + ) + .classList.contains("o-focused"), + "chat window of channel 3 should have focus" + ); } - }, - }); + ); - assert.containsN( - document.body, - '.o_ChatWindow', - 2, - "2 chat windows should be visible" - ); - assert.containsNone( - document.body, - `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, - "chat window for Channel #12 should be hidden" - ); - assert.containsOnce( - document.body, - '.o_ChatWindowHiddenMenuView', - "chat window hidden menu should be displayed" - ); - assert.verifySteps( - [`rpc:/mail/channel/messages:${mailChannelId1}`, `rpc:/mail/channel/messages:${mailChannelId2}`], - "messages should be fetched for the two visible chat windows" - ); + QUnit.test("chat window: switch on TAB", async function (assert) { + assert.expect(10); - await click('.o_ChatWindowHiddenMenuView_dropdownToggle'); - assert.containsOnce( - document.body, - '.o_ChatWindowHiddenMenuItemView', - "1 hidden chat window should be listed in hidden menu" - ); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "channel1" }, + { name: "channel2" }, + ]); + const { click } = await start(); - await click('.o_ChatWindowHiddenMenuItemView_chatWindowHeader'); - assert.containsN( - document.body, - '.o_ChatWindow', - 2, - "2 chat windows should still be visible" - ); - assert.containsOnce( - document.body, - `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, - "chat window for Channel #12 should now be visible" - ); - assert.verifySteps( - [`rpc:/mail/channel/messages:${mailChannelId3}`], - "messages should now be fetched for Channel #12" - ); -}); + await click(`.o_MessagingMenu_toggler`); + await click(` + .o_MessagingMenu_dropdownMenu + .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]`); -QUnit.test('new message separator is shown in a chat window of a chat on receiving new message if there is a history of conversation', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { messaging } = await start(); - - // simulate receiving a message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - })); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "a chat window should be visible after receiving a new message from a chat" - ); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "chat window should have 2 messages" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should display 'new messages' separator in the conversation, from reception of new messages" - ); -}); - -QUnit.test('new message separator is not shown in a chat window of a chat on receiving new message if there is no history of conversation', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); - const { messaging } = await start(); - - // simulate receiving a message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - })); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator in the conversation of a chat on receiving new message if there is no history of conversation" - ); -}); - -QUnit.test('focusing a chat window of a chat should make new message separator disappear [REQUIRE FOCUS]', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { afterEvent, messaging } = await start(); - - // simulate receiving a message - await afterNextRender(() => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - })); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should display 'new messages' separator in the conversation, from reception of new messages" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => document.querySelector('.o_ComposerTextInputView_textarea').focus(), - message: "should wait until last seen by current partner message id changed", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' + assert.containsOnce(document.body, ".o_ChatWindow", "Only 1 chatWindow must be opened"); + const chatWindow = document.querySelector(".o_ChatWindow"); + assert.strictEqual( + chatWindow.querySelector(".o_ChatWindowHeaderView_name").textContent, + "channel1", + "The name of the only chatWindow should be 'channel1' (channel with ID 1)" + ); + assert.strictEqual( + chatWindow.querySelector(".o_ComposerTextInputView_textarea"), + document.activeElement, + "The chatWindow composer must have focus" ); - }, - })); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "new message separator should no longer be shown, after focus on composer text input of chat window" - ); -}); -QUnit.test('chat window should open when receiving a new DM', async function (assert) { - assert.expect(1); + await afterNextRender(() => + triggerEvent( + chatWindow.querySelector(".o_ChatWindow .o_ComposerTextInputView_textarea"), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + chatWindow.querySelector(".o_ChatWindow .o_ComposerTextInputView_textarea"), + document.activeElement, + "The chatWindow composer still has focus" + ); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const resUsersId1 = pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - uuid: 'channel11uuid', - }); - const { messaging } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(` + .o_MessagingMenu_dropdownMenu + .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]`); - // simulate receiving the first message on channel 11 - await afterNextRender(() => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "new message", - uuid: 'channel11uuid', - }, - })); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "a chat window should be open now that current user received a new message" - ); -}); + assert.containsN(document.body, ".o_ChatWindow", 2, "2 chatWindows must be opened"); + const chatWindows = document.querySelectorAll(".o_ChatWindow"); + assert.strictEqual( + chatWindows[0].querySelector(".o_ChatWindowHeaderView_name").textContent, + "channel1", + "The name of the 1st chatWindow should be 'channel1' (channel with ID 1)" + ); + assert.strictEqual( + chatWindows[1].querySelector(".o_ChatWindowHeaderView_name").textContent, + "channel2", + "The name of the 2nd chatWindow should be 'channel2' (channel with ID 2)" + ); + assert.strictEqual( + chatWindows[1].querySelector(".o_ComposerTextInputView_textarea"), + document.activeElement, + "The 2nd chatWindow composer must have focus (channel with ID 2)" + ); -QUnit.test('chat window should remain folded when new message is received', async function (assert) { - assert.expect(1); + await afterNextRender(() => + triggerEvent( + chatWindows[1].querySelector(".o_ComposerTextInputView_textarea"), + "keydown", + { key: "Tab" } + ) + ); + assert.containsN( + document.body, + ".o_ChatWindow", + 2, + "2 chatWindows should still be opened" + ); + assert.strictEqual( + chatWindows[0].querySelector(".o_ComposerTextInputView_textarea"), + document.activeElement, + "The 1st chatWindow composer must have focus (channel with ID 1)" + ); + }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'folded', - is_minimized: true, - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { - partner_id: resPartnerId1, - }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); + QUnit.test( + "chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]", + async function (assert) { + /** + * InnerWith computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 + */ + assert.expect(6); - const { messaging } = await start(); - // simulate receiving a new message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "New Message 2", - uuid: 'channel-10-uuid', - }, - })); - assert.hasClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should remain folded" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + ]); + patchUiSize({ width: 1920 }); + await start(); + assert.containsN( + document.body, + ".o_ChatWindow .o_ComposerTextInputView_textarea", + 3, + "initialy, 3 chat windows should be present" + ); + assert.containsNone( + document.body, + ".o_ChatWindow.o-folded", + "all 3 chat windows should be open" + ); -QUnit.test('should not have chat window hidden menu in mobile (transition from 3 chat windows in desktop to mobile)', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: 900px) - * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1080px - * - * Not enough space for 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 - * Enough space for 2 visible chat windows + hidden menu: - * 10 + 340 + 5 + 340 + 5 + 170 + 10 = 880 < 900 - */ - assert.expect(1); + await afterNextRender(() => { + document + .querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ) + .focus(); + }); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "The chatWindow with visible-index 2 should have the focus" + ); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }, { name: 'mailChannel3' }]); - patchUiSize({ width: 900 }); // enough to fit 2 chat windows + hidden menu - const { click, messaging } = await start(); - // open, from systray menu, chat windows of channels with id 1, 2, 3 - await click('.o_MessagingMenu_toggler'); - await click(` + await afterNextRender(() => + triggerEvent( + document.querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "after pressing tab on the chatWindow with visible-index 2, the chatWindow with visible-index 1 should have focus" + ); + + await afterNextRender(() => + triggerEvent( + document.querySelector( + ".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea" + ), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "after pressing tab on the chat window with visible-index 1, the chatWindow with visible-index 0 should have focus" + ); + + await afterNextRender(() => + triggerEvent( + document.querySelector( + ".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea" + ), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "the chatWindow with visible-index 2 should have the focus after pressing tab on the chatWindow with visible-index 0" + ); + } + ); + + QUnit.test( + "chat window with a thread: keep scroll position in message list on folded", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 100; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_NotificationListView_preview").click(), + message: + "should wait until channel scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + // Set a scroll position to chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; + }, + message: + "should wait until channel scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "verify chat window initial scrollTop" + ); + + // fold chat window + await click(".o_ChatWindow_header"); + assert.containsNone( + document.body, + ".o_ThreadView", + "chat window should be folded so no ThreadView should be present" + ); + + // unfold chat window + await afterNextRender(() => + afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_ChatWindow_header").click(), + message: "should wait until channel restored its scroll position to 142", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }) + ); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same when chat window is unfolded" + ); + } + ); + + QUnit.test( + "chat window should scroll to the newly posted message just after posting it", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + for (let i = 0; i < 10; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { insertText } = await start(); + + // Set content of the composer of the chat window + await insertText(".o_ComposerTextInputView_textarea", "WOLOLO"); + // Send a new message in the chatwindow to trigger the scroll + await afterNextRender(() => + triggerEvent( + document.querySelector(".o_ChatWindow .o_ComposerTextInputView_textarea"), + "keydown", + { key: "Enter" } + ) + ); + const messageList = document.querySelector(".o_MessageListView"); + assert.ok( + isScrolledToBottom(messageList), + "chat window should scroll to the newly posted message just after posting it" + ); + } + ); + + QUnit.test( + 'chat window: post message on non-mailing channel with "CTRL-Enter" keyboard shortcut for small screen size', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + patchUiSize({ size: SIZES.SM }); + const { click, insertText } = await start(); + + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { + ctrlKey: true, + key: "Enter", + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in channel after posting message from pressing 'CTRL-Enter' in text input of composer for small screen" + ); + } + ); + + QUnit.test( + "chat window with a thread: keep scroll position in message list on toggle discuss when folded", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 100; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, openDiscuss, openView } = await start(); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_NotificationListView_preview").click(), + message: + "should wait until channel scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + // Set a scroll position to chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + (document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142), + message: + "should wait until channel scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + // fold chat window + await click(".o_ChatWindow_header"); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have any chat window after opening discuss" + ); + + await openView({ + res_id: mailChannelId1, + res_model: "mail.channel", + views: [[false, "list"]], + }); + // unfold chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_ChatWindow_header").click(), + message: + "should wait until channel restored its scroll position to the last saved value (142)", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after closing discuss" + ); + } + ); + + QUnit.test("chat window does not fetch messages if hidden", async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1080px + * + * Enough space for 2 visible chat windows, and one hidden chat window: + * 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 + * 2 visible chat windows + hidden menu: + * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 + */ + assert.expect(11); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Channel #10", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Channel #11", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Channel #12", + }, + ]); + patchUiSize({ width: 900 }); + const { click } = await start({ + mockRPC(route, args) { + if (route === "/mail/channel/messages") { + const { channel_id } = args; + assert.step(`rpc:/mail/channel/messages:${channel_id}`); + } + }, + }); + + assert.containsN(document.body, ".o_ChatWindow", 2, "2 chat windows should be visible"); + assert.containsNone( + document.body, + `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, + "chat window for Channel #12 should be hidden" + ); + assert.containsOnce( + document.body, + ".o_ChatWindowHiddenMenuView", + "chat window hidden menu should be displayed" + ); + assert.verifySteps( + [ + `rpc:/mail/channel/messages:${mailChannelId1}`, + `rpc:/mail/channel/messages:${mailChannelId2}`, + ], + "messages should be fetched for the two visible chat windows" + ); + + await click(".o_ChatWindowHiddenMenuView_dropdownToggle"); + assert.containsOnce( + document.body, + ".o_ChatWindowHiddenMenuItemView", + "1 hidden chat window should be listed in hidden menu" + ); + + await click(".o_ChatWindowHiddenMenuItemView_chatWindowHeader"); + assert.containsN( + document.body, + ".o_ChatWindow", + 2, + "2 chat windows should still be visible" + ); + assert.containsOnce( + document.body, + `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, + "chat window for Channel #12 should now be visible" + ); + assert.verifySteps( + [`rpc:/mail/channel/messages:${mailChannelId3}`], + "messages should now be fetched for Channel #12" + ); + }); + + QUnit.test( + "new message separator is shown in a chat window of a chat on receiving new message if there is a history of conversation", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { messaging } = await start(); + + // simulate receiving a message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "a chat window should be visible after receiving a new message from a chat" + ); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "chat window should have 2 messages" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should display 'new messages' separator in the conversation, from reception of new messages" + ); + } + ); + + QUnit.test( + "new message separator is not shown in a chat window of a chat on receiving new message if there is no history of conversation", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + const { messaging } = await start(); + + // simulate receiving a message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }) + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator in the conversation of a chat on receiving new message if there is no history of conversation" + ); + } + ); + + QUnit.test( + "focusing a chat window of a chat should make new message separator disappear [REQUIRE FOCUS]", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, messaging } = await start(); + + // simulate receiving a message + await afterNextRender(() => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }) + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should display 'new messages' separator in the conversation, from reception of new messages" + ); + + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + document.querySelector(".o_ComposerTextInputView_textarea").focus(), + message: + "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }) + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "new message separator should no longer be shown, after focus on composer text input of chat window" + ); + } + ); + + QUnit.test("chat window should open when receiving a new DM", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const resUsersId1 = pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel11uuid", + }); + const { messaging } = await start(); + + // simulate receiving the first message on channel 11 + await afterNextRender(() => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "new message", + uuid: "channel11uuid", + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "a chat window should be open now that current user received a new message" + ); + }); + + QUnit.test( + "chat window should remain folded when new message is received", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "folded", + is_minimized: true, + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [ + 0, + 0, + { + partner_id: resPartnerId1, + }, + ], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + + const { messaging } = await start(); + // simulate receiving a new message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "New Message 2", + uuid: "channel-10-uuid", + }, + }) + ); + assert.hasClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should remain folded" + ); + } + ); + + QUnit.test( + "should not have chat window hidden menu in mobile (transition from 3 chat windows in desktop to mobile)", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1080px + * + * Not enough space for 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 + * Enough space for 2 visible chat windows + hidden menu: + * 10 + 340 + 5 + 340 + 5 + 170 + 10 = 880 < 900 + */ + assert.expect(1); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv[ + "mail.channel" + ].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + { name: "mailChannel3" }, + ]); + patchUiSize({ width: 900 }); // enough to fit 2 chat windows + hidden menu + const { click, messaging } = await start(); + // open, from systray menu, chat windows of channels with id 1, 2, 3 + await click(".o_MessagingMenu_toggler"); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `); - await click('.o_MessagingMenu_toggler'); - await click(` + await click(".o_MessagingMenu_toggler"); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `); - await click('.o_MessagingMenu_toggler'); - await click(` + await click(".o_MessagingMenu_toggler"); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId3}"] `); - // simulate resize to go into mobile - await afterNextRender( - () => messaging.device.update({ - globalWindowInnerWidth: 300, - isMobileDevice: true, - isSmall: true, - sizeClass: 0, // XS - }), - ); - assert.containsNone( - document.body, - '.o_ChatWindowManager_hiddenMenu', - "should not have any chat window hidden menu in mobile (transition from desktop having 3 chat windows (2 visible, 1 hidden)", - ); -}); - -}); + // simulate resize to go into mobile + await afterNextRender(() => + messaging.device.update({ + globalWindowInnerWidth: 300, + isMobileDevice: true, + isSmall: true, + sizeClass: 0, // XS + }) + ); + assert.containsNone( + document.body, + ".o_ChatWindowManager_hiddenMenu", + "should not have any chat window hidden menu in mobile (transition from desktop having 3 chat windows (2 visible, 1 hidden)" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js index ac5531fba52..dd89e90f213 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js @@ -1,260 +1,320 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chatter_suggested_recipients_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chatter_suggested_recipients_tests.js"); -QUnit.test("suggest recipient on 'Send message' composer", async function (assert) { - assert.expect(1); + QUnit.test("suggest recipient on 'Send message' composer", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be", partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView', - "Should display a list of suggested recipients after opening the composer from 'Send message' button" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ + email_cc: "john@test.be", + partner_ids: [resPartnerId1], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView", + "Should display a list of suggested recipients after opening the composer from 'Send message' button" + ); + }); -QUnit.test("with 3 or less suggested recipients: no 'show more' button", async function (assert) { - assert.expect(1); + QUnit.test( + "with 3 or less suggested recipients: no 'show more' button", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be", partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.containsNone( - document.body, - '.o_ComposerSuggestedRecipientListView_showMore', - "should not display 'show more' button with 3 or less suggested recipients" - ); -}); - -QUnit.test("display reason for suggested recipient on mouse over", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - const partnerTitle = document.querySelector(`.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"]`).getAttribute('title'); - assert.strictEqual( - partnerTitle, - "Add as recipient and follower (reason: Email partner)", - "must display reason for suggested recipient on mouse over", - ); -}); - -QUnit.test("suggested recipient without partner are unchecked by default", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be" }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - const checkboxUnchecked = document.querySelector('.o_ComposerSuggestedRecipientView:not([data-partner-id]) input[type=checkbox]'); - assert.notOk( - checkboxUnchecked.checked, - "suggested recipient without partner must be unchecked by default", - ); -}); - -QUnit.test("suggested recipient with partner are checked by default", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - const checkboxChecked = document.querySelector(`.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"] input[type=checkbox]`); - assert.ok( - checkboxChecked.checked, - "suggested recipient with partner must be checked by default", - ); -}); - -QUnit.test("more than 3 suggested recipients: display only 3 and 'show more' button", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView_showMore', - "more than 3 suggested recipients display 'show more' button" - ); -}); - -QUnit.test("more than 3 suggested recipients: show all of them on click 'show more' button", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - await click(`.o_ComposerSuggestedRecipientListView_showMore`); - assert.containsN( - document.body, - '.o_ComposerSuggestedRecipientView', - 4, - "more than 3 suggested recipients: show all of them on click 'show more' button" - ); -}); - -QUnit.test("more than 3 suggested recipients -> click 'show more' -> 'show less' button", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - await click(`.o_ComposerSuggestedRecipientListView_showMore`); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView_showLess', - "more than 3 suggested recipients -> click 'show more' -> 'show less' button" - ); -}); - -QUnit.test("suggested recipients list display 3 suggested recipient and 'show more' button when 'show less' button is clicked", async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - await click(`.o_ComposerSuggestedRecipientListView_showMore`); - await click(`.o_ComposerSuggestedRecipientListView_showLess`); - assert.containsN( - document.body, - '.o_ComposerSuggestedRecipientView', - 3, - "suggested recipient list should display 3 suggested recipients after clicking on 'show less'." - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView_showMore', - "suggested recipient list should containt a 'show More' button after clicking on 'show less'." - ); -}); - -QUnit.test("suggested recipients should not be notified when posting an internal note", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] }); - const { click, insertText, openView } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.strictEqual( - args.post_data.partner_ids.length, - 0, - "post data should not contain suggested recipients when posting an internal note" + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ + email_cc: "john@test.be", + partner_ids: [resPartnerId1], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.containsNone( + document.body, + ".o_ComposerSuggestedRecipientListView_showMore", + "should not display 'show more' button with 3 or less suggested recipients" ); } - }, - }); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonLogNote`); - await insertText('.o_ComposerTextInputView_textarea', "Dummy Message"); - await click('.o_ComposerView_buttonSend'); -}); + ); -}); + QUnit.test("display reason for suggested recipient on mouse over", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ partner_ids: [resPartnerId1] }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + const partnerTitle = document + .querySelector( + `.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"]` + ) + .getAttribute("title"); + assert.strictEqual( + partnerTitle, + "Add as recipient and follower (reason: Email partner)", + "must display reason for suggested recipient on mouse over" + ); + }); + + QUnit.test( + "suggested recipient without partner are unchecked by default", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resFakeId1 = pyEnv["res.fake"].create({ email_cc: "john@test.be" }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + const checkboxUnchecked = document.querySelector( + ".o_ComposerSuggestedRecipientView:not([data-partner-id]) input[type=checkbox]" + ); + assert.notOk( + checkboxUnchecked.checked, + "suggested recipient without partner must be unchecked by default" + ); + } + ); + + QUnit.test( + "suggested recipient with partner are checked by default", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ partner_ids: [resPartnerId1] }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + const checkboxChecked = document.querySelector( + `.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"] input[type=checkbox]` + ); + assert.ok( + checkboxChecked.checked, + "suggested recipient with partner must be checked by default" + ); + } + ); + + QUnit.test( + "more than 3 suggested recipients: display only 3 and 'show more' button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView_showMore", + "more than 3 suggested recipients display 'show more' button" + ); + } + ); + + QUnit.test( + "more than 3 suggested recipients: show all of them on click 'show more' button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + await click(`.o_ComposerSuggestedRecipientListView_showMore`); + assert.containsN( + document.body, + ".o_ComposerSuggestedRecipientView", + 4, + "more than 3 suggested recipients: show all of them on click 'show more' button" + ); + } + ); + + QUnit.test( + "more than 3 suggested recipients -> click 'show more' -> 'show less' button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + await click(`.o_ComposerSuggestedRecipientListView_showMore`); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView_showLess", + "more than 3 suggested recipients -> click 'show more' -> 'show less' button" + ); + } + ); + + QUnit.test( + "suggested recipients list display 3 suggested recipient and 'show more' button when 'show less' button is clicked", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + await click(`.o_ComposerSuggestedRecipientListView_showMore`); + await click(`.o_ComposerSuggestedRecipientListView_showLess`); + assert.containsN( + document.body, + ".o_ComposerSuggestedRecipientView", + 3, + "suggested recipient list should display 3 suggested recipients after clicking on 'show less'." + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView_showMore", + "suggested recipient list should containt a 'show More' button after clicking on 'show less'." + ); + } + ); + + QUnit.test( + "suggested recipients should not be notified when posting an internal note", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ partner_ids: [resPartnerId1] }); + const { click, insertText, openView } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.strictEqual( + args.post_data.partner_ids.length, + 0, + "post data should not contain suggested recipients when posting an internal note" + ); + } + }, + }); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonLogNote`); + await insertText(".o_ComposerTextInputView_textarea", "Dummy Message"); + await click(".o_ComposerView_buttonSend"); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js index 9eedbd02f69..ca6fa36e807 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js @@ -6,537 +6,560 @@ import { dropFiles, nextAnimationFrame, start, - startServer -} from '@mail/../tests/helpers/test_utils'; + startServer, +} from "@mail/../tests/helpers/test_utils"; -import { file } from 'web.test_utils'; +import { file } from "web.test_utils"; const { createFile } = file; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chatter', {}, function () { -QUnit.module('chatter_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chatter", {}, function () { + QUnit.module("chatter_tests.js"); -QUnit.test('base rendering when chatter has no attachment', async function (assert) { - assert.expect(6); + QUnit.test("base rendering when chatter has no attachment", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - for (let i = 0; i < 60; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - res_id: resPartnerId1, + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + for (let i = 0; i < 60; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId1, + }); + } + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_thread`).length, + 1, + "should have a thread in the chatter" + ); + assert.containsOnce( + document.body, + `.o_Chatter_thread[data-thread-id="${resPartnerId1}"][data-thread-model="res.partner"]`, + "chatter should have the right thread." + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 30, + "the first 30 messages of thread should be loaded" + ); + }); + + QUnit.test("base rendering when chatter has no record", async function (assert) { + assert.expect(9); + + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_thread`).length, + 1, + "should have a thread in the chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 1, + "should have a message" + ); + assert.strictEqual( + document.querySelector(`.o_MessageView_content`).textContent, + "Creating a new record...", + "should have the 'Creating a new record ...' message" + ); + assert.containsNone( + document.body, + ".o_MessageListView_loadMore", + "should not have the 'load more' button" + ); + + await click(".o_MessageView"); + assert.strictEqual( + document.querySelectorAll(`.o_MessageActionList`).length, + 1, + "should action list in message" + ); + assert.containsNone( + document.body, + ".o_MessageActionView", + "should not have any action in action list of message" + ); + }); + + QUnit.test("base rendering when chatter has attachments", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + }); + + QUnit.test("show attachment box", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, + 1, + "should have an attachments button in chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 1, + "attachments button should have a counter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + + await click(`.o_ChatterTopbar_buttonToggleAttachments`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 1, + "should have an attachment box in the chatter" + ); + }); + + QUnit.test("chatter: drop attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { afterEvent, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + let files = [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), + await createFile({ + content: "hello, worlduh", + contentType: "text/plain", + name: "text2.txt", + }), + ]; + await afterNextRender(() => dragenterFiles(document.querySelector(".o_Chatter"))); + assert.ok(document.querySelector(".o_Chatter_dropZone"), "should have a drop zone"); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView`).length, + 0, + "should have no attachment before files are dropped" + ); + + await afterNextRender(() => + afterEvent({ + eventName: "o-file-uploader-upload", + func: () => dropFiles(document.querySelector(".o_Chatter_dropZone"), files), + message: "should wait until files are uploaded", + predicate: ({ files: uploadedFiles }) => uploadedFiles === files, + }) + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, + 2, + "should have 2 attachments in the attachment box after files dropped" + ); + + await afterNextRender(() => dragenterFiles(document.querySelector(".o_Chatter"))); + files = [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text3.txt", + }), + ]; + await afterNextRender(() => + afterEvent({ + eventName: "o-file-uploader-upload", + func: () => dropFiles(document.querySelector(".o_Chatter_dropZone"), files), + message: "should wait until files are uploaded", + predicate: ({ files: uploadedFiles }) => uploadedFiles === files, + }) + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, + 3, + "should have 3 attachments in the attachment box after files dropped" + ); + }); + + QUnit.test( + "composer show/hide on log note/send message [REQUIRE FOCUS]", + async function (assert) { + assert.expect(10); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, + 1, + "should have a send message button" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, + 1, + "should have a log note button" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 0, + "should not have a composer" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 1, + "should have a composer" + ); + assert.hasClass( + document.querySelector(".o_Chatter_composer"), + "o-focused", + "composer 'send message' in chatter should have focus just after being displayed" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 1, + "should still have a composer" + ); + assert.hasClass( + document.querySelector(".o_Chatter_composer"), + "o-focused", + "composer 'log note' in chatter should have focus just after being displayed" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 0, + "should have no composer anymore" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 1, + "should have a composer" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 0, + "should have no composer anymore" + ); + } + ); + + QUnit.test( + "should display subject when subject is not the same as the thread name", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId1, + subject: "Salutations, voyageur", + }); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + } + ); + + QUnit.test( + "should not display subject when subject is the same as the thread name", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Salutations, voyageur", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId1, + subject: "Salutations, voyageur", + }); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject of the message" + ); + } + ); + + QUnit.test( + "should not display user notification messages in chatter", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.message"].create({ + message_type: "user_notification", + model: "res.partner", + res_id: resPartnerId1, + }); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsNone( + document.body, + ".o_MessageView", + "should display no messages" + ); + } + ); + + QUnit.test('post message with "CTRL-Enter" keyboard shortcut', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, insertText, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in chatter" + ); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { + ctrlKey: true, + key: "Enter", + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in chatter after posting message from pressing 'CTRL-Enter' in text input of composer" + ); + }); + + QUnit.test('post message with "META-Enter" keyboard shortcut', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, insertText, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in chatter" + ); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { + key: "Enter", + metaKey: true, + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in channel after posting message from pressing 'META-Enter' in text input of composer" + ); + }); + + QUnit.test( + 'do not post message with "Enter" keyboard shortcut', + async function (assert) { + // Note that test doesn't assert Enter makes a newline, because this + // default browser cannot be simulated with just dispatching + // programmatically crafted events... + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, insertText, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in chatter" + ); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "Test"); + const kevt = new window.KeyboardEvent("keydown", { key: "Enter" }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_MessageView", + "should still not have any message in mailing channel after pressing 'Enter' in text input of composer" + ); + } + ); }); - } - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_thread`).length, - 1, - "should have a thread in the chatter" - ); - assert.containsOnce( - document.body, - `.o_Chatter_thread[data-thread-id="${resPartnerId1}"][data-thread-model="res.partner"]`, - "chatter should have the right thread." - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 30, - "the first 30 messages of thread should be loaded" - ); -}); - -QUnit.test('base rendering when chatter has no record', async function (assert) { - assert.expect(9); - - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_thread`).length, - 1, - "should have a thread in the chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 1, - "should have a message" - ); - assert.strictEqual( - document.querySelector(`.o_MessageView_content`).textContent, - "Creating a new record...", - "should have the 'Creating a new record ...' message" - ); - assert.containsNone( - document.body, - '.o_MessageListView_loadMore', - "should not have the 'load more' button" - ); - - await click('.o_MessageView'); - assert.strictEqual( - document.querySelectorAll(`.o_MessageActionList`).length, - 1, - "should action list in message" - ); - assert.containsNone( - document.body, - '.o_MessageActionView', - "should not have any action in action list of message" - ); -}); - -QUnit.test('base rendering when chatter has attachments', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); -}); - -QUnit.test('show attachment box', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, - 1, - "should have an attachments button in chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 1, - "attachments button should have a counter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); - - await click(`.o_ChatterTopbar_buttonToggleAttachments`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 1, - "should have an attachment box in the chatter" - ); -}); - -QUnit.test('chatter: drop attachments', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { afterEvent, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - let files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }), - await createFile({ - content: 'hello, worlduh', - contentType: 'text/plain', - name: 'text2.txt', - }), - ]; - await afterNextRender(() => dragenterFiles(document.querySelector('.o_Chatter'))); - assert.ok( - document.querySelector('.o_Chatter_dropZone'), - "should have a drop zone" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView`).length, - 0, - "should have no attachment before files are dropped" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-file-uploader-upload', - func: () => dropFiles(document.querySelector('.o_Chatter_dropZone'), files), - message: "should wait until files are uploaded", - predicate: ({ files: uploadedFiles }) => uploadedFiles === files, - })); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, - 2, - "should have 2 attachments in the attachment box after files dropped" - ); - - await afterNextRender(() => dragenterFiles(document.querySelector('.o_Chatter'))); - files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text3.txt', - }) - ]; - await afterNextRender(() => afterEvent({ - eventName: 'o-file-uploader-upload', - func: () => dropFiles(document.querySelector('.o_Chatter_dropZone'), files), - message: "should wait until files are uploaded", - predicate: ({ files: uploadedFiles }) => uploadedFiles === files, - })); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, - 3, - "should have 3 attachments in the attachment box after files dropped" - ); -}); - -QUnit.test('composer show/hide on log note/send message [REQUIRE FOCUS]', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, - 1, - "should have a send message button" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, - 1, - "should have a log note button" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 0, - "should not have a composer" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 1, - "should have a composer" - ); - assert.hasClass( - document.querySelector('.o_Chatter_composer'), - 'o-focused', - "composer 'send message' in chatter should have focus just after being displayed" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 1, - "should still have a composer" - ); - assert.hasClass( - document.querySelector('.o_Chatter_composer'), - 'o-focused', - "composer 'log note' in chatter should have focus just after being displayed" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 0, - "should have no composer anymore" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 1, - "should have a composer" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 0, - "should have no composer anymore" - ); -}); - -QUnit.test('should display subject when subject is not the same as the thread name', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - res_id: resPartnerId1, - subject: "Salutations, voyageur", - }); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('should not display subject when subject is the same as the thread name', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Salutations, voyageur" }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - res_id: resPartnerId1, - subject: "Salutations, voyageur", - }); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject of the message" - ); -}); - -QUnit.test('should not display user notification messages in chatter', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.message'].create({ - message_type: 'user_notification', - model: 'res.partner', - res_id: resPartnerId1, - }); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsNone( - document.body, - '.o_MessageView', - "should display no messages" - ); -}); - -QUnit.test('post message with "CTRL-Enter" keyboard shortcut', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, insertText, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in chatter" - ); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { ctrlKey: true, key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in chatter after posting message from pressing 'CTRL-Enter' in text input of composer" - ); -}); - -QUnit.test('post message with "META-Enter" keyboard shortcut', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, insertText, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in chatter" - ); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { key: "Enter", metaKey: true }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in channel after posting message from pressing 'META-Enter' in text input of composer" - ); -}); - -QUnit.test('do not post message with "Enter" keyboard shortcut', async function (assert) { - // Note that test doesn't assert Enter makes a newline, because this - // default browser cannot be simulated with just dispatching - // programmatically crafted events... - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, insertText, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in chatter" - ); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - const kevt = new window.KeyboardEvent('keydown', { key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_MessageView', - "should still not have any message in mailing channel after pressing 'Enter' in text input of composer" - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js index c33e5e8bad0..795c1adea5f 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js @@ -1,567 +1,579 @@ /** @odoo-module **/ -import { afterNextRender, nextAnimationFrame, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { + afterNextRender, + nextAnimationFrame, + start, + startServer, +} from "@mail/../tests/helpers/test_utils"; -import { makeTestPromise } from 'web.test_utils'; +import { makeTestPromise } from "web.test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chatter_topbar_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chatter_topbar_tests.js"); -QUnit.test('base rendering', async function (assert) { - assert.expect(7); + QUnit.test("base rendering", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, - 1, - "should have a send message button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, - 1, - "should have a log note button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonScheduleActivity`).length, - 1, - "should have a schedule activity button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_followerListMenu`).length, - 1, - "should have a follower menu" - ); -}); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, + 1, + "should have a send message button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, + 1, + "should have a log note button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonScheduleActivity`).length, + 1, + "should have a schedule activity button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 0, + "attachments button should not have a loader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_followerListMenu`).length, + 1, + "should have a follower menu" + ); + }); -QUnit.test('base disabled rendering', async function (assert) { - assert.expect(6); + QUnit.test("base disabled rendering", async function (assert) { + assert.expect(6); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, - "send message button should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, - "log note button should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonScheduleActivity`).disabled, - "schedule activity should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, - "attachments button should be disabled" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader" - ); -}); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, + "send message button should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, + "log note button should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonScheduleActivity`).disabled, + "schedule activity should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, + "attachments button should be disabled" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 0, + "attachments button should not have a loader" + ); + }); -QUnit.test('attachment loading is delayed', async function (assert) { - assert.expect(4); + QUnit.test("attachment loading is delayed", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { advanceTime, openView } = await start({ - hasTimeControl: true, - loadingBaseDelayDuration: 100, - async mockRPC(route) { - if (route.includes('/mail/thread/data')) { - await makeTestPromise(); // simulate long loading + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { advanceTime, openView } = await start({ + hasTimeControl: true, + loadingBaseDelayDuration: 100, + async mockRPC(route) { + if (route.includes("/mail/thread/data")) { + await makeTestPromise(); // simulate long loading + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 0, + "attachments button should not have a loader yet" + ); + + await afterNextRender(async () => advanceTime(100)); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 1, + "attachments button should now have a loader" + ); + }); + + QUnit.test("attachment counter while loading attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { openView } = await start({ + async mockRPC(route) { + if (route.includes("/mail/thread/data")) { + await makeTestPromise(); // simulate long loading + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 1, + "attachments button should have a loader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 0, + "attachments button should not have a counter" + ); + }); + + QUnit.test( + "attachment counter transition when attachments become loaded)", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const attachmentPromise = makeTestPromise(); + const { openView } = await start({ + async mockRPC(route) { + if (route.includes("/mail/thread/data")) { + await attachmentPromise; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`) + .length, + 1, + "attachments button should have a loader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 0, + "attachments button should not have a counter" + ); + + await afterNextRender(() => attachmentPromise.resolve()); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`) + .length, + 0, + "attachments button should not have a loader" + ); } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader yet" - ); + QUnit.test("attachment counter without attachments", async function (assert) { + assert.expect(2); - await afterNextRender(async () => advanceTime(100)); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 1, - "attachments button should now have a loader" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); -QUnit.test('attachment counter while loading attachments', async function (assert) { - assert.expect(4); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { openView } = await start({ - async mockRPC(route) { - if (route.includes('/mail/thread/data')) { - await makeTestPromise(); // simulate long loading + QUnit.test("attachment counter with attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 1, + "attachments button should have a counter" + ); + assert.strictEqual( + document.querySelector(`.o_ChatterTopbar_buttonAttachmentsCount`).textContent, + "2", + 'attachment counter content should contain "2 files"' + ); + }); + + QUnit.test( + "composer state conserved when clicking on another topbar button", + async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsOnce( + document.body, + `.o_ChatterTopbar`, + "should have a chatter topbar" + ); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonSendMessage`, + "should have a send message button in chatter menu" + ); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonLogNote`, + "should have a log note button in chatter menu" + ); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonAddAttachments`, + "should have an attachments button in chatter menu" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonLogNote.o-active`, + "log button should now be active" + ); + assert.containsNone( + document.body, + `.o_ChatterTopbar_buttonSendMessage.o-active`, + "send message button should not be active" + ); + + document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).click(); + await nextAnimationFrame(); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonLogNote.o-active`, + "log button should still be active" + ); + assert.containsNone( + document.body, + `.o_ChatterTopbar_buttonSendMessage.o-active`, + "send message button should still be not active" + ); } - } + ); + + QUnit.test("rendering with multiple partner followers", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + { message_follower_ids: [1, 2] }, + ]); + pyEnv["mail.followers"].create([ + { + name: "Jean Michang", + partner_id: resPartnerId2, + res_id: resPartnerId3, + res_model: "res.partner", + }, + { + name: "Eden Hazard", + partner_id: resPartnerId1, + res_id: resPartnerId3, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId3, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" + ); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be opened" + ); + assert.containsN( + document.body, + ".o_FollowerView", + 2, + "exactly two followers should be listed" + ); + assert.containsN( + document.body, + ".o_FollowerView_name", + 2, + "exactly two follower names should be listed" + ); + assert.strictEqual( + document.querySelectorAll(".o_FollowerView_name")[0].textContent.trim(), + "Jean Michang", + "first follower is 'Jean Michang'" + ); + assert.strictEqual( + document.querySelectorAll(".o_FollowerView_name")[1].textContent.trim(), + "Eden Hazard", + "second follower is 'Eden Hazard'" + ); + }); + + QUnit.test("log note/send message switching", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonSendMessage", + "should have a 'Send Message' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonLogNote", + "should have a 'Log Note' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should be active" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should be active" + ); + }); + + QUnit.test("log note toggling", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonLogNote", + "should have a 'Log Note' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should be active" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + }); + + QUnit.test("send message toggling", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonSendMessage", + "should have a 'Send Message' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should be active" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + }); }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 1, - "attachments button should have a loader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 0, - "attachments button should not have a counter" - ); -}); - -QUnit.test('attachment counter transition when attachments become loaded)', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const attachmentPromise = makeTestPromise(); - const { openView } = await start({ - async mockRPC(route) { - if (route.includes('/mail/thread/data')) { - await attachmentPromise; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 1, - "attachments button should have a loader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 0, - "attachments button should not have a counter" - ); - - await afterNextRender(() => attachmentPromise.resolve()); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader" - ); -}); - -QUnit.test('attachment counter without attachments', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); -}); - -QUnit.test('attachment counter with attachments', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 1, - "attachments button should have a counter" - ); - assert.strictEqual( - document.querySelector(`.o_ChatterTopbar_buttonAttachmentsCount`).textContent, - '2', - 'attachment counter content should contain "2 files"' - ); -}); - -QUnit.test('composer state conserved when clicking on another topbar button', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsOnce( - document.body, - `.o_ChatterTopbar`, - "should have a chatter topbar" - ); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonSendMessage`, - "should have a send message button in chatter menu" - ); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonLogNote`, - "should have a log note button in chatter menu" - ); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonAddAttachments`, - "should have an attachments button in chatter menu" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonLogNote.o-active`, - "log button should now be active" - ); - assert.containsNone( - document.body, - `.o_ChatterTopbar_buttonSendMessage.o-active`, - "send message button should not be active" - ); - - document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).click(); - await nextAnimationFrame(); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonLogNote.o-active`, - "log button should still be active" - ); - assert.containsNone( - document.body, - `.o_ChatterTopbar_buttonSendMessage.o-active`, - "send message button should still be not active" - ); -}); - -QUnit.test('rendering with multiple partner followers', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { name: 'resPartner1' }, - { name: 'resPartner2' }, - { message_follower_ids: [1, 2] }, - ]); - pyEnv['mail.followers'].create([ - { - name: "Jean Michang", - partner_id: resPartnerId2, - res_id: resPartnerId3, - res_model: 'res.partner', - }, - { - name: "Eden Hazard", - partner_id: resPartnerId1, - res_id: resPartnerId3, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId3, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be opened" - ); - assert.containsN( - document.body, - '.o_FollowerView', - 2, - "exactly two followers should be listed" - ); - assert.containsN( - document.body, - '.o_FollowerView_name', - 2, - "exactly two follower names should be listed" - ); - assert.strictEqual( - document.querySelectorAll('.o_FollowerView_name')[0].textContent.trim(), - "Jean Michang", - "first follower is 'Jean Michang'" - ); - assert.strictEqual( - document.querySelectorAll('.o_FollowerView_name')[1].textContent.trim(), - "Eden Hazard", - "second follower is 'Eden Hazard'" - ); -}); - -QUnit.test('log note/send message switching', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonSendMessage', - "should have a 'Send Message' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonLogNote', - "should have a 'Log Note' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should be active" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should be active" - ); -}); - -QUnit.test('log note toggling', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonLogNote', - "should have a 'Log Note' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should be active" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); -}); - -QUnit.test('send message toggling', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonSendMessage', - "should have a 'Send Message' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should be active" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js index bb17690c57d..9bb3ae79b55 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js @@ -1,100 +1,99 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_canned_response_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_canned_response_tests.js"); -QUnit.test('canned response suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("canned response suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ - source: 'hello', - substitution: "Hello, how are you?", + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello, how are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", ":hello"); + assert.containsOnce( + document.body, + `.o_ComposerSuggestionView`, + "Canned response suggestion should be present" + ); + }); + + QUnit.test("canned response suggestion correct data", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello, how are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", ":hello"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Canned response source should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, + "hello", + "Canned response source should be displayed" + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part2", + "Canned response substitution should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, + "Hello, how are you?", + "Canned response substitution should be displayed" + ); + }); + + QUnit.test("canned response suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello, how are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", ":hello"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "should be active initially" + ); + }); }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', ":hello"); - assert.containsOnce( - document.body, - `.o_ComposerSuggestionView`, - "Canned response suggestion should be present" - ); -}); - -QUnit.test('canned response suggestion correct data', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ - source: 'hello', - substitution: "Hello, how are you?", - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', ":hello"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Canned response source should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, - "hello", - "Canned response source should be displayed" - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part2', - "Canned response substitution should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, - "Hello, how are you?", - "Canned response substitution should be displayed" - ); -}); - -QUnit.test('canned response suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ - source: 'hello', - substitution: "Hello, how are you?", - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', ":hello"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js index 5aed5ceff3d..353621894e4 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js @@ -1,78 +1,77 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_channel_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_channel_tests.js"); -QUnit.test('channel mention suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("channel mention suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#my-channel"); + assert.containsOnce( + document.body, + `.o_ComposerSuggestionView`, + "Channel mention suggestion should be present" + ); + }); + + QUnit.test("channel mention suggestion correct data", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#General"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Channel name should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, + "General", + "Channel name should be displayed" + ); + }); + + QUnit.test("channel mention suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#my-channel"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "should be active initially" + ); + }); }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#my-channel"); - assert.containsOnce( - document.body, - `.o_ComposerSuggestionView`, - "Channel mention suggestion should be present" - ); -}); - -QUnit.test('channel mention suggestion correct data', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#General"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Channel name should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, - "General", - "Channel name should be displayed" - ); -}); - -QUnit.test('channel mention suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#my-channel"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js index d2c420d2e02..8033276fb99 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js @@ -1,88 +1,87 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_command_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_command_tests.js"); -QUnit.test('command suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("command suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "/who"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "Command suggestion should be present" + ); + }); + + QUnit.test("command suggestion correct data", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "/who"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Command name should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, + "who", + "Command name should be displayed" + ); + assert.containsOnce( + document.querySelector(".o_ComposerSuggestionView"), + ".o_ComposerSuggestionView_part2", + "Command help should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, + "List users in the current channel", + "Command help should be displayed" + ); + }); + + QUnit.test("command suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "/who"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "1st suggestion should be active initially" + ); + }); }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "/who"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "Command suggestion should be present", - ); -}); - -QUnit.test('command suggestion correct data', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "/who"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Command name should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, - "who", - "Command name should be displayed" - ); - assert.containsOnce( - document.querySelector('.o_ComposerSuggestionView'), - '.o_ComposerSuggestionView_part2', - "Command help should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, - "List users in the current channel", - "Command help should be displayed" - ); -}); - -QUnit.test('command suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "/who"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "1st suggestion should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js index 90cb4319879..d668e0f4c44 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js @@ -1,123 +1,122 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_partner_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_partner_tests.js"); -QUnit.test('partner mention suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("partner mention suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ - email: "demo_user@odoo.com", - im_status: 'online', - name: 'Demo User', + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "demo_user@odoo.com", + im_status: "online", + name: "Demo User", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@demo"); + assert.containsOnce( + document.body, + `.o_ComposerSuggestionView`, + "Partner mention suggestion should be present" + ); + }); + + QUnit.test("partner mention suggestion correct data", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "demo_user@odoo.com", + im_status: "online", + name: "Demo User", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@demo"); + assert.containsOnce( + document.querySelector(".o_ComposerSuggestionView"), + ".o_PersonaImStatusIconView", + "Partner's im_status should be displayed" + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Partner's name should be present" + ); + assert.strictEqual( + document.querySelector(".o_ComposerSuggestionView_part1").textContent, + "Demo User", + "Partner's name should be displayed" + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part2", + "Partner's email should be present" + ); + assert.strictEqual( + document.querySelector(".o_ComposerSuggestionView_part2").textContent, + "(demo_user@odoo.com)", + "Partner's email should be displayed" + ); + }); + + QUnit.test("partner mention suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "demo_user@odoo.com", + im_status: "online", + name: "Demo User", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@demo"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "should be active initially" + ); + }); }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@demo"); - assert.containsOnce( - document.body, - `.o_ComposerSuggestionView`, - "Partner mention suggestion should be present" - ); -}); - -QUnit.test('partner mention suggestion correct data', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ - email: "demo_user@odoo.com", - im_status: 'online', - name: 'Demo User', - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@demo"); - assert.containsOnce( - document.querySelector('.o_ComposerSuggestionView'), - '.o_PersonaImStatusIconView', - "Partner's im_status should be displayed" - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Partner's name should be present" - ); - assert.strictEqual( - document.querySelector('.o_ComposerSuggestionView_part1').textContent, - "Demo User", - "Partner's name should be displayed" - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part2', - "Partner's email should be present" - ); - assert.strictEqual( - document.querySelector('.o_ComposerSuggestionView_part2').textContent, - "(demo_user@odoo.com)", - "Partner's email should be displayed" - ); -}); - -QUnit.test('partner mention suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ - email: "demo_user@odoo.com", - im_status: 'online', - name: 'Demo User', - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@demo"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js index d4cbb91f882..5dd155b7441 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js @@ -8,1593 +8,1725 @@ import { pasteFiles, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { - file, - makeTestPromise, -} from 'web.test_utils'; +import { file, makeTestPromise } from "web.test_utils"; const { createFile, inputFiles } = file; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_tests.js"); -QUnit.test('composer text input: basic rendering when posting a message', async function (assert) { - assert.expect(5); + QUnit.test( + "composer text input: basic rendering when posting a message", + async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const { click, openFormView } = await start(); - await openFormView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - }); - await click('.o_ChatterTopbar_buttonSendMessage'); + const pyEnv = await startServer(); + const { click, openFormView } = await start(); + await openFormView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + }); + await click(".o_ChatterTopbar_buttonSendMessage"); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView').length, - 1, - "should have composer in discuss thread" - ); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView_textInput').length, - 1, - "should have text input inside discuss thread composer" - ); - assert.ok( - document.querySelector('.o_ComposerView_textInput').classList.contains('o_ComposerTextInputView'), - "composer text input of composer should be a ComposerTextIput component" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, - 1, - "should have editable part inside composer text input" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Send a message to followers...", - "should have 'Send a message to followers...' as placeholder composer text input" - ); -}); - -QUnit.test('composer text input: basic rendering when logging note', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const { click, openFormView } = await start(); - await openFormView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - }); - await click('.o_ChatterTopbar_buttonLogNote'); - - assert.strictEqual( - document.querySelectorAll('.o_ComposerView').length, - 1, - "should have composer in discuss thread" - ); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView_textInput').length, - 1, - "should have text input inside discuss thread composer" - ); - assert.ok( - document.querySelector('.o_ComposerView_textInput').classList.contains('o_ComposerTextInputView'), - "composer text input of composer should be a ComposerTextIput component" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, - 1, - "should have editable part inside composer text input" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Log an internal note...", - "should have 'Log an internal note...' as placeholder in composer text input if composer is log" - ); -}); - -QUnit.test('composer text input: basic rendering when linked thread is a mail.channel', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView').length, - 1, - "should have composer in discuss thread" - ); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView_textInput').length, - 1, - "should have text input inside discuss thread composer" - ); - assert.ok( - document.querySelector('.o_ComposerView_textInput').classList.contains('o_ComposerTextInputView'), - "composer text input of composer should be a ComposerTextIput component" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, - 1, - "should have editable part inside composer text input" - ); -}); - -QUnit.test('composer text input placeholder should contain channel name when thread does not have specific correspondent', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel', name: 'General' }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Message #General...", - "should have 'Message #General...' as placeholder for composer text input when thread does not have specific correspondent" - ); -}); - -QUnit.test('composer text input placeholder should contain correspondent name when thread has exactly one correspondent', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Marc Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Message Marc Demo...", - "should have 'Message Marc Demo...' as placeholder for composer text input when thread has exactly one correspondent" - ); -}); - -QUnit.test('add an emoji', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "😊", - "emoji should be inserted in the composer text input" - ); -}); - -QUnit.test('add an emoji after some text', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "Blabla"); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "Blabla", - "composer text input should have text only initially" - ); - - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "Blabla😊", - "emoji should be inserted after the text" - ); -}); - -QUnit.test('add emoji replaces (keyboard) text selection', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1, }, - }, - }); - await openDiscuss(); - const composerTextInputTextArea = document.querySelector(`.o_ComposerTextInputView_textarea`); - await insertText('.o_ComposerTextInputView_textarea', "Blabla"); - assert.strictEqual( - composerTextInputTextArea.value, - "Blabla", - "composer text input should have text only initially" - ); - - // simulate selection of all the content by keyboard - composerTextInputTextArea.setSelectionRange(0, composerTextInputTextArea.value.length); - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "😊", - "whole text selection should have been replaced by emoji" - ); -}); - -QUnit.test('display canned response suggestions on typing ":"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "Canned responses suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display canned response suggestions on typing ':'" - ); -}); - -QUnit.test('use a canned response', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "canned response suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a canned response suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "Hello! How are you? ", - "text content of composer should have canned response + additional whitespace afterwards" - ); -}); - -QUnit.test('use a canned response some text', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "canned response suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a canned response suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "bluhbluh Hello! How are you? ", - "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" - ); -}); - -QUnit.test('add an emoji after a canned response', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "canned response suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a canned response suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "Hello! How are you? ", - "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" - ); - - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "Hello! How are you? 😊", - "text content of composer should have previous canned response substitution and selected emoji just after" - ); -}); - -QUnit.test('display channel mention suggestions on typing "#"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "channel mention suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display channel mention suggestions on typing '#'" - ); -}); - -QUnit.test('mention a channel', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "channel mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a channel mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "#General ", - "text content of composer should have mentioned channel + additional whitespace afterwards" - ); -}); - -QUnit.test('mention a channel after some text', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "channel mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a channel mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "bluhbluh #General ", - "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" - ); -}); - -QUnit.test('add an emoji after a channel mention', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a channel mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "#General ", - "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" - ); - - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "#General 😊", - "text content of composer should have previous channel mention and selected emoji just after" - ); -}); - -QUnit.test('display command suggestions on typing "/"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "command suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display command suggestions on typing '/'" - ); -}); - -QUnit.test('do not send typing notification on typing "/" command', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView").length, + 1, + "should have composer in discuss thread" + ); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView_textInput").length, + 1, + "should have text input inside discuss thread composer" + ); + assert.ok( + document + .querySelector(".o_ComposerView_textInput") + .classList.contains("o_ComposerTextInputView"), + "composer text input of composer should be a ComposerTextIput component" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, + 1, + "should have editable part inside composer text input" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Send a message to followers...", + "should have 'Send a message to followers...' as placeholder composer text input" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - assert.verifySteps([], "No rpc done"); -}); + QUnit.test( + "composer text input: basic rendering when logging note", + async function (assert) { + assert.expect(5); -QUnit.test('do not send typing notification on typing after selecting suggestion from "/" command', async function (assert) { - assert.expect(1); + const pyEnv = await startServer(); + const { click, openFormView } = await start(); + await openFormView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + }); + await click(".o_ChatterTopbar_buttonLogNote"); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView").length, + 1, + "should have composer in discuss thread" + ); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView_textInput").length, + 1, + "should have text input inside discuss thread composer" + ); + assert.ok( + document + .querySelector(".o_ComposerView_textInput") + .classList.contains("o_ComposerTextInputView"), + "composer text input of composer should be a ComposerTextIput component" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, + 1, + "should have editable part inside composer text input" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Log an internal note...", + "should have 'Log an internal note...' as placeholder in composer text input if composer is log" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - await click('.o_ComposerSuggestionView'); - await insertText('.o_ComposerTextInputView_textarea', " is user?"); - assert.verifySteps([], "No rpc done"); -}); + QUnit.test( + "composer text input: basic rendering when linked thread is a mail.channel", + async function (assert) { + assert.expect(4); -QUnit.test('use a command for a specific channel type', async function (assert) { - assert.expect(3); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView").length, + 1, + "should have composer in discuss thread" + ); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView_textInput").length, + 1, + "should have text input inside discuss thread composer" + ); + assert.ok( + document + .querySelector(".o_ComposerView_textInput") + .classList.contains("o_ComposerTextInputView"), + "composer text input of composer should be a ComposerTextIput component" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, + 1, + "should have editable part inside composer text input" + ); + } + ); - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'chat' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); + QUnit.test( + "composer text input placeholder should contain channel name when thread does not have specific correspondent", + async function (assert) { + assert.expect(1); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "command suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "/who ", - "text content of composer should have used command + additional whitespace afterwards" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Message #General...", + "should have 'Message #General...' as placeholder for composer text input when thread does not have specific correspondent" + ); + } + ); -QUnit.test('command suggestion should only open if command is the first character', async function (assert) { - assert.expect(4); + QUnit.test( + "composer text input placeholder should contain correspondent name when thread has exactly one correspondent", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "command suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "should not have a command suggestion" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Marc Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Message Marc Demo...", + "should have 'Message Marc Demo...' as placeholder for composer text input when thread has exactly one correspondent" + ); + } + ); -QUnit.test('add an emoji after a command', async function (assert) { - assert.expect(4); + QUnit.test("add an emoji", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "😊", + "emoji should be inserted in the composer text input" + ); + }); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "command suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "/who ", - "text content of composer should have previous content + used command + additional whitespace afterwards" - ); + QUnit.test("add an emoji after some text", async function (assert) { + assert.expect(2); - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "/who 😊", - "text content of composer should have previous command and selected emoji just after" - ); -}); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "Blabla"); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "Blabla", + "composer text input should have text only initially" + ); -QUnit.test('display partner mention suggestions on typing "@"', async function (assert) { - assert.expect(3); + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "Blabla😊", + "emoji should be inserted after the text" + ); + }); - const pyEnv = await startServer(); + QUnit.test("add emoji replaces (keyboard) text selection", async function (assert) { + assert.expect(2); - const resPartnerId1 = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const resPartnerId2 = pyEnv['res.partner'].create({ email: "testpartner2@odoo.com", name: "TestPartner2" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + const composerTextInputTextArea = document.querySelector( + `.o_ComposerTextInputView_textarea` + ); + await insertText(".o_ComposerTextInputView_textarea", "Blabla"); + assert.strictEqual( + composerTextInputTextArea.value, + "Blabla", + "composer text input should have text only initially" + ); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "mention suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', "@"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display mention suggestions on typing '@'" - ); - assert.containsOnce( - document.body, - '.dropdown-divider', - "should have a separator" - ); -}); + // simulate selection of all the content by keyboard + composerTextInputTextArea.setSelectionRange(0, composerTextInputTextArea.value.length); + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "😊", + "whole text selection should have been replaced by emoji" + ); + }); -QUnit.test('mention a partner', async function (assert) { - assert.expect(4); + QUnit.test('display canned response suggestions on typing ":"', async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', '@Te'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "@TestPartner ", - "text content of composer should have mentioned partner + additional whitespace afterwards" - ); -}); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "Canned responses suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display canned response suggestions on typing ':'" + ); + }); -QUnit.test('mention a partner after some text', async function (assert) { - assert.expect(5); + QUnit.test("use a canned response", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "bluhbluh @TestPartner ", - "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" - ); -}); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "canned response suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a canned response suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "Hello! How are you? ", + "text content of composer should have canned response + additional whitespace afterwards" + ); + }); -QUnit.test('add an emoji after a partner mention', async function (assert) { - assert.expect(5); + QUnit.test("use a canned response some text", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "@TestPartner ", - "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" - ); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "canned response suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a canned response suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "bluhbluh Hello! How are you? ", + "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" + ); + }); - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "@TestPartner 😊", - "text content of composer should have previous mention and selected emoji just after" - ); -}); + QUnit.test("add an emoji after a canned response", async function (assert) { + assert.expect(5); -QUnit.test('composer: add an attachment', async function (assert) { - assert.expect(2); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "canned response suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a canned response suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "Hello! How are you? ", + "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" + ); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.ok( - document.querySelector('.o_ComposerView_attachmentList'), - "should have an attachment list" - ); - assert.ok( - document.querySelector(`.o_ComposerView .o_AttachmentCard`), - "should have an attachment" - ); -}); + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "Hello! How are you? 😊", + "text content of composer should have previous canned response substitution and selected emoji just after" + ); + }); -QUnit.test('composer: drop attachments', async function (assert) { - assert.expect(4); + QUnit.test('display channel mention suggestions on typing "#"', async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - }); - await openDiscuss(); - const files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }), - await createFile({ - content: 'hello, worlduh', - contentType: 'text/plain', - name: 'text2.txt', - }), - ]; - await afterNextRender(() => dragenterFiles(document.querySelector('.o_ComposerView'))); - assert.ok( - document.querySelector('.o_ComposerView_dropZone'), - "should have a drop zone" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 0, - "should have no attachment before files are dropped" - ); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - await afterNextRender(() => - dropFiles(document.querySelector('.o_ComposerView_dropZone'), files) - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 2, - "should have 2 attachments in the composer after files dropped" - ); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "channel mention suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display channel mention suggestions on typing '#'" + ); + }); - await afterNextRender(() => dragenterFiles(document.querySelector('.o_ComposerView'))); - await afterNextRender(async () => - dropFiles( - document.querySelector('.o_ComposerView_dropZone'), - [ + QUnit.test("mention a channel", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "channel mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a channel mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "#General ", + "text content of composer should have mentioned channel + additional whitespace afterwards" + ); + }); + + QUnit.test("mention a channel after some text", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "channel mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a channel mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "bluhbluh #General ", + "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" + ); + }); + + QUnit.test("add an emoji after a channel mention", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a channel mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "#General ", + "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" + ); + + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "#General 😊", + "text content of composer should have previous channel mention and selected emoji just after" + ); + }); + + QUnit.test('display command suggestions on typing "/"', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "command suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display command suggestions on typing '/'" + ); + }); + + QUnit.test( + 'do not send typing notification on typing "/" command', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "/"); + assert.verifySteps([], "No rpc done"); + } + ); + + QUnit.test( + 'do not send typing notification on typing after selecting suggestion from "/" command', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "/"); + await click(".o_ComposerSuggestionView"); + await insertText(".o_ComposerTextInputView_textarea", " is user?"); + assert.verifySteps([], "No rpc done"); + } + ); + + QUnit.test("use a command for a specific channel type", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "chat" }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "command suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "/who ", + "text content of composer should have used command + additional whitespace afterwards" + ); + }); + + QUnit.test( + "command suggestion should only open if command is the first character", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "command suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "should not have a command suggestion" + ); + } + ); + + QUnit.test("add an emoji after a command", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "channel" }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "command suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "/who ", + "text content of composer should have previous content + used command + additional whitespace afterwards" + ); + + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "/who 😊", + "text content of composer should have previous command and selected emoji just after" + ); + }); + + QUnit.test('display partner mention suggestions on typing "@"', async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const resPartnerId2 = pyEnv["res.partner"].create({ + email: "testpartner2@odoo.com", + name: "TestPartner2", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "mention suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", "@"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display mention suggestions on typing '@'" + ); + assert.containsOnce(document.body, ".dropdown-divider", "should have a separator"); + }); + + QUnit.test("mention a partner", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "@TestPartner ", + "text content of composer should have mentioned partner + additional whitespace afterwards" + ); + }); + + QUnit.test("mention a partner after some text", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "bluhbluh @TestPartner ", + "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" + ); + }); + + QUnit.test("add an emoji after a partner mention", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "@TestPartner ", + "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" + ); + + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "@TestPartner 😊", + "text content of composer should have previous mention and selected emoji just after" + ); + }); + + QUnit.test("composer: add an attachment", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) + ); + assert.ok( + document.querySelector(".o_ComposerView_attachmentList"), + "should have an attachment list" + ); + assert.ok( + document.querySelector(`.o_ComposerView .o_AttachmentCard`), + "should have an attachment" + ); + }); + + QUnit.test("composer: drop attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + const files = [ await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text3.txt', - }) - ] - ) - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 3, - "should have 3 attachments in the box after files dropped" - ); -}); + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), + await createFile({ + content: "hello, worlduh", + contentType: "text/plain", + name: "text2.txt", + }), + ]; + await afterNextRender(() => dragenterFiles(document.querySelector(".o_ComposerView"))); + assert.ok( + document.querySelector(".o_ComposerView_dropZone"), + "should have a drop zone" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 0, + "should have no attachment before files are dropped" + ); -QUnit.test('composer: paste attachments', async function (assert) { - assert.expect(2); + await afterNextRender(() => + dropFiles(document.querySelector(".o_ComposerView_dropZone"), files) + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 2, + "should have 2 attachments in the composer after files dropped" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - }); - await openDiscuss(); - const files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }) - ]; - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 0, - "should not have any attachment in the composer before paste" - ); + await afterNextRender(() => dragenterFiles(document.querySelector(".o_ComposerView"))); + await afterNextRender(async () => + dropFiles(document.querySelector(".o_ComposerView_dropZone"), [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text3.txt", + }), + ]) + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 3, + "should have 3 attachments in the box after files dropped" + ); + }); - await afterNextRender(() => - pasteFiles(document.querySelector('.o_ComposerTextInputView'), files) - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 1, - "should have 1 attachment in the composer after paste" - ); -}); + QUnit.test("composer: paste attachments", async function (assert) { + assert.expect(2); -QUnit.test('composer text input cleared on message post', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + const files = [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), + ]; + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 0, + "should not have any attachment in the composer before paste" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); + await afterNextRender(() => + pasteFiles(document.querySelector(".o_ComposerTextInputView"), files) + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 1, + "should have 1 attachment in the composer after paste" + ); + }); + + QUnit.test("composer text input cleared on message post", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + } + }, + }); + await openDiscuss(); + // Type message + await insertText(".o_ComposerTextInputView_textarea", "test message"); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "test message", + "should have inserted text content in editable" + ); + + // Send message + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["message_post"]); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "should have no content in composer input after posting message" + ); + }); + + QUnit.test("composer with thread typing notification status", async function (assert) { + assert.expect(2); + + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_ComposerView_threadTextualTypingStatus", + "Composer should have a thread textual typing status bar" + ); + assert.strictEqual( + document.body.querySelector(".o_ComposerView_threadTextualTypingStatus") + .textContent, + "", + "By default, thread textual typing status bar should be empty" + ); + }); + + QUnit.test( + "current partner notify is typing to other thread members", + async function (assert) { + assert.expect(2); + + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "a"); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner typing status" + ); } - }, - }); - await openDiscuss(); - // Type message - await insertText('.o_ComposerTextInputView_textarea', "test message"); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "test message", - "should have inserted text content in editable" - ); + ); - // Send message - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['message_post']); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "should have no content in composer input after posting message" - ); -}); + QUnit.test( + "current partner is typing should not translate on textual typing status", + async function (assert) { + assert.expect(3); -QUnit.test('composer with thread typing notification status', async function (assert) { - assert.expect(2); + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + hasTimeControl: true, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "a"); - assert.containsOnce( - document.body, - '.o_ComposerView_threadTextualTypingStatus', - "Composer should have a thread textual typing status bar" - ); - assert.strictEqual( - document.body.querySelector('.o_ComposerView_threadTextualTypingStatus').textContent, - "", - "By default, thread textual typing status bar should be empty" - ); -}); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner typing status" + ); -QUnit.test('current partner notify is typing to other thread members', async function (assert) { - assert.expect(2); - - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + await nextAnimationFrame(); + assert.strictEqual( + document.body.querySelector(".o_ComposerView_threadTextualTypingStatus") + .textContent, + "", + "Thread textual typing status bar should not display current partner is typing" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', 'a'); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner typing status" - ); -}); + QUnit.test( + "current partner notify no longer is typing to thread members after 5 seconds inactivity", + async function (assert) { + assert.expect(4); -QUnit.test('current partner is typing should not translate on textual typing status', async function (assert) { - assert.expect(3); + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { advanceTime, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + hasTimeControl: true, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - hasTimeControl: true, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + await insertText(".o_ComposerTextInputView_textarea", "a"); + + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner is typing" + ); + + await advanceTime(5 * 1000); + assert.verifySteps( + ["notify_typing:false"], + "should have notified current partner no longer is typing (inactive for 5 seconds)" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', 'a'); + QUnit.test( + "current partner notify is typing again to other members every 50s of long continuous typing", + async function (assert) { + assert.expect(4); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner typing status" - ); + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { advanceTime, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + hasTimeControl: true, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); - await nextAnimationFrame(); - assert.strictEqual( - document.body.querySelector('.o_ComposerView_threadTextualTypingStatus').textContent, - "", - "Thread textual typing status bar should not display current partner is typing" - ); -}); + await insertText(".o_ComposerTextInputView_textarea", "a"); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner is typing" + ); -QUnit.test('current partner notify no longer is typing to thread members after 5 seconds inactivity', async function (assert) { - assert.expect(4); + // simulate current partner typing a character every 2.5 seconds for 50 seconds straight. + let totalTimeElapsed = 0; + const elapseTickTime = 2.5 * 1000; + while (totalTimeElapsed < 50 * 1000) { + await insertText(".o_ComposerTextInputView_textarea", "a"); + totalTimeElapsed += elapseTickTime; + await advanceTime(elapseTickTime); + } - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { advanceTime, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - hasTimeControl: true, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner is still typing after 50s of straight typing" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', 'a'); + QUnit.test( + "composer: send button is disabled if attachment upload is not finished", + async function (assert) { + assert.expect(8); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner is typing" - ); + const pyEnv = await startServer(); + const attachmentUploadedPromise = makeTestPromise(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + await attachmentUploadedPromise; + } + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file, + ]) + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard", + "should have an attachment after a file has been input" + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard.o-isUploading", + "attachment displayed is being uploaded" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonSend", + "composer send button should be displayed" + ); + assert.ok( + !!document.querySelector(".o_ComposerView_buttonSend").attributes.disabled, + "composer send button should be disabled as attachment is not yet uploaded" + ); - await advanceTime(5 * 1000); - assert.verifySteps( - ['notify_typing:false'], - "should have notified current partner no longer is typing (inactive for 5 seconds)" - ); -}); - -QUnit.test('current partner notify is typing again to other members every 50s of long continuous typing', async function (assert) { - assert.expect(4); - - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { advanceTime, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - hasTimeControl: true, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + // simulates attachment finishes uploading + await afterNextRender(() => attachmentUploadedPromise.resolve()); + assert.containsOnce( + document.body, + ".o_AttachmentCard", + "should have only one attachment" + ); + assert.containsNone( + document.body, + ".o_AttachmentCard.o-isUploading", + "attachment displayed should be uploaded" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonSend", + "composer send button should still be present" + ); + assert.ok( + !document.querySelector(".o_ComposerView_buttonSend").attributes.disabled, + "composer send button should be enabled as attachment is now uploaded" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', "a"); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner is typing" - ); + QUnit.test( + "remove an attachment from composer does not need any confirmation", + async function (assert) { + assert.expect(3); - // simulate current partner typing a character every 2.5 seconds for 50 seconds straight. - let totalTimeElapsed = 0; - const elapseTickTime = 2.5 * 1000; - while (totalTimeElapsed < 50 * 1000) { - await insertText('.o_ComposerTextInputView_textarea', 'a'); - totalTimeElapsed += elapseTickTime; - await advanceTime(elapseTickTime); - } + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-file-uploader-upload", + func() { + inputFiles( + messaging.discuss.threadView.composerView.fileUploader.fileInput, + [file] + ); + }, + message: "should wait until files are uploaded", + predicate: ({ files: uploadedFiles }) => uploadedFiles[0] === file, + }) + ); + assert.containsOnce( + document.body, + ".o_ComposerView_attachmentList", + "should have an attachment list" + ); + assert.containsOnce( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should have only one attachment" + ); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner is still typing after 50s of straight typing" - ); -}); + const attachmentLocalId = document.querySelector(".o_AttachmentCard").dataset.id; + await afterNextRender(() => + afterEvent({ + eventName: "o-attachment-deleted", + func() { + document.querySelector(".o_AttachmentCard_asideItemUnlink").click(); + }, + message: "should wait until attachment is deleted", + predicate: ({ attachment }) => { + return attachment.localId === attachmentLocalId; + }, + }) + ); -QUnit.test('composer: send button is disabled if attachment upload is not finished', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const attachmentUploadedPromise = makeTestPromise(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - await attachmentUploadedPromise; + assert.containsNone( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should not have any attachment left after unlinking the only one" + ); } - } - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should have an attachment after a file has been input" - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard.o-isUploading', - "attachment displayed is being uploaded" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonSend', - "composer send button should be displayed" - ); - assert.ok( - !!document.querySelector('.o_ComposerView_buttonSend').attributes.disabled, - "composer send button should be disabled as attachment is not yet uploaded" - ); + ); - // simulates attachment finishes uploading - await afterNextRender(() => attachmentUploadedPromise.resolve()); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should have only one attachment" - ); - assert.containsNone( - document.body, - '.o_AttachmentCard.o-isUploading', - "attachment displayed should be uploaded" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonSend', - "composer send button should still be present" - ); - assert.ok( - !document.querySelector('.o_ComposerView_buttonSend').attributes.disabled, - "composer send button should be enabled as attachment is now uploaded" - ); -}); + QUnit.test("remove an uploading attachment", async function (assert) { + assert.expect(4); -QUnit.test('remove an attachment from composer does not need any confirmation', async function (assert) { - assert.expect(3); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, openDiscuss, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + // simulates uploading indefinitely + await new Promise(() => {}); + } + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) + ); + assert.containsOnce( + document.body, + ".o_ComposerView_attachmentList", + "should have an attachment list" + ); + assert.containsOnce( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should have only one attachment" + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard.o-isUploading", + "should have an uploading attachment" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-file-uploader-upload', - func() { - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]); - }, - message: 'should wait until files are uploaded', - predicate: ({ files: uploadedFiles }) => uploadedFiles[0] === file, - })); - assert.containsOnce( - document.body, - '.o_ComposerView_attachmentList', - "should have an attachment list" - ); - assert.containsOnce( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should have only one attachment" - ); + await click(".o_AttachmentCard_asideItemUnlink"); + assert.containsNone( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should not have any attachment left after unlinking uploading one" + ); + }); - const attachmentLocalId = document.querySelector('.o_AttachmentCard').dataset.id; - await afterNextRender(() => afterEvent({ - eventName: 'o-attachment-deleted', - func() { - document.querySelector('.o_AttachmentCard_asideItemUnlink').click(); - }, - message: "should wait until attachment is deleted", - predicate: ({ attachment }) => { - return attachment.localId === attachmentLocalId; - }, - })); + QUnit.test("remove an uploading attachment aborts upload", async function (assert) { + assert.expect(1); - assert.containsNone( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should not have any attachment left after unlinking the only one" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, openDiscuss, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + // simulates uploading indefinitely + await new Promise(() => {}); + } + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) + ); + assert.containsOnce(document.body, ".o_AttachmentCard", "should contain an attachment"); + const attachmentLocalId = document.querySelector(".o_AttachmentCard").dataset.id; -QUnit.test('remove an uploading attachment', async function (assert) { - assert.expect(4); + await afterEvent({ + eventName: "o-attachment-upload-abort", + func: () => { + document.querySelector(".o_AttachmentCard_asideItemUnlink").click(); + }, + message: "attachment upload request should have been aborted", + predicate: ({ attachment }) => { + return attachment.localId === attachmentLocalId; + }, + }); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, openDiscuss, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - // simulates uploading indefinitely - await new Promise(() => {}); + QUnit.test( + "Show a default status in the recipient status text when the thread doesn't have a name.", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + await click(".o_ChatterTopbar_buttonSendMessage"); + assert.strictEqual( + document + .querySelector(".o_ComposerView_followers") + .textContent.replace(/\s+/g, ""), + "To:Followersofthisdocument", + 'Composer should display "To: Followers of this document" if the thread as no name.' + ); } - } - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.containsOnce( - document.body, - '.o_ComposerView_attachmentList', - "should have an attachment list" - ); - assert.containsOnce( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should have only one attachment" - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard.o-isUploading', - "should have an uploading attachment" - ); + ); - await click('.o_AttachmentCard_asideItemUnlink'); - assert.containsNone( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should not have any attachment left after unlinking uploading one" - ); -}); + QUnit.test("Show a thread name in the recipient status text.", async function (assert) { + assert.expect(1); -QUnit.test('remove an uploading attachment aborts upload', async function (assert) { - assert.expect(1); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "test name" }); + const { click, messaging, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + // hack: provide awareness of name (not received in usual chatter flow) + messaging.models["Thread"].insert({ + id: resPartnerId1, + model: "res.partner", + name: "test name", + }); + await click(".o_ChatterTopbar_buttonSendMessage"); + assert.strictEqual( + document.querySelector(".o_ComposerView_followers").textContent.replace(/\s+/g, ""), + 'To:Followersof"testname"', + "basic rendering when sending a message to the followers and thread does have a name" + ); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, openDiscuss, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - // simulates uploading indefinitely - await new Promise(() => {}); + QUnit.test( + "send message only once when button send is clicked twice quickly", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + } + }, + }); + await openDiscuss(); + // Type message + await insertText(".o_ComposerTextInputView_textarea", "test message"); + + await afterNextRender(() => { + document.querySelector(`.o_ComposerView_buttonSend`).click(); + document.querySelector(`.o_ComposerView_buttonSend`).click(); + }); + assert.verifySteps(["message_post"], "The message has been posted only once"); } - } - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should contain an attachment" - ); - const attachmentLocalId = document.querySelector('.o_AttachmentCard').dataset.id; + ); - await afterEvent({ - eventName: 'o-attachment-upload-abort', - func: () => { - document.querySelector('.o_AttachmentCard_asideItemUnlink').click(); - }, - message: "attachment upload request should have been aborted", - predicate: ({ attachment }) => { - return attachment.localId === attachmentLocalId; - }, - }); -}); + QUnit.test( + "[technical] does not crash when an attachment is removed before its upload starts", + async function (assert) { + // Uploading multiple files uploads attachments one at a time, this test + // ensures that there is no crash when an attachment is destroyed before its + // upload started. + assert.expect(1); -QUnit.test("Show a default status in the recipient status text when the thread doesn't have a name.", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - await click('.o_ChatterTopbar_buttonSendMessage'); - assert.strictEqual( - document.querySelector('.o_ComposerView_followers').textContent.replace(/\s+/g, ''), - "To:Followersofthisdocument", - "Composer should display \"To: Followers of this document\" if the thread as no name." - ); -}); - -QUnit.test("Show a thread name in the recipient status text.", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "test name" }); - const { click, messaging, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - // hack: provide awareness of name (not received in usual chatter flow) - messaging.models['Thread'].insert({ id: resPartnerId1, model: 'res.partner', name: "test name" }); - await click('.o_ChatterTopbar_buttonSendMessage'); - assert.strictEqual( - document.querySelector('.o_ComposerView_followers').textContent.replace(/\s+/g, ''), - "To:Followersof\"testname\"", - "basic rendering when sending a message to the followers and thread does have a name" - ); -}); - -QUnit.test('send message only once when button send is clicked twice quickly', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); + const pyEnv = await startServer(); + // Promise to block attachment uploading + const uploadPromise = makeTestPromise(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + await uploadPromise; + } + }, + }); + await openDiscuss(); + const file1 = await createFile({ + name: "text1.txt", + content: "hello, world", + contentType: "text/plain", + }); + const file2 = await createFile({ + name: "text2.txt", + content: "hello, world", + contentType: "text/plain", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file1, + file2, + ]) + ); + await afterNextRender(() => { + Array.from(document.querySelectorAll("div")) + .find((el) => el.textContent === "text2.txt") + .closest(".o_AttachmentCard") + .querySelector(".o_AttachmentCard_asideItemUnlink") + .click(); + }); + // Simulates the completion of the upload of the first attachment + await afterNextRender(() => uploadPromise.resolve()); + assert.containsOnce( + document.body, + '.o_AttachmentCard:contains("text1.txt")', + "should only have the first attachment after cancelling the second attachment" + ); } - }, - }); - await openDiscuss(); - // Type message - await insertText('.o_ComposerTextInputView_textarea', "test message"); + ); - await afterNextRender(() => { - document.querySelector(`.o_ComposerView_buttonSend`).click(); - document.querySelector(`.o_ComposerView_buttonSend`).click(); - }); - assert.verifySteps( - ['message_post'], - "The message has been posted only once" - ); -}); + QUnit.test( + 'send button on mail.channel should have "Send" as label', + async function (assert) { + assert.expect(1); -QUnit.test('[technical] does not crash when an attachment is removed before its upload starts', async function (assert) { - // Uploading multiple files uploads attachments one at a time, this test - // ensures that there is no crash when an attachment is destroyed before its - // upload started. - assert.expect(1); - - const pyEnv = await startServer(); - // Promise to block attachment uploading - const uploadPromise = makeTestPromise(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - await uploadPromise; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(".o_ComposerView_buttonSend").textContent, + "Send", + "Send button of mail.channel composer should have 'Send' as label" + ); } - }, + ); }); - await openDiscuss(); - const file1 = await createFile({ - name: 'text1.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - const file2 = await createFile({ - name: 'text2.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - await afterNextRender(() => - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - [file1, file2] - ) - ); - await afterNextRender(() => { - Array.from(document.querySelectorAll('div')) - .find(el => el.textContent === 'text2.txt') - .closest('.o_AttachmentCard') - .querySelector('.o_AttachmentCard_asideItemUnlink') - .click(); - } - ); - // Simulates the completion of the upload of the first attachment - await afterNextRender(() => uploadPromise.resolve()); - assert.containsOnce( - document.body, - '.o_AttachmentCard:contains("text1.txt")', - "should only have the first attachment after cancelling the second attachment" - ); -}); - -QUnit.test('send button on mail.channel should have "Send" as label', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector('.o_ComposerView_buttonSend').textContent, - "Send", - "Send button of mail.channel composer should have 'Send' as label", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js index 945ebc35024..2e814fb978d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js @@ -1,49 +1,44 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { makeDeferred } from "@mail/utils/deferred"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('dialog_manager_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("dialog_manager_tests.js"); -QUnit.test('[technical] messaging not created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(1); + QUnit.test("[technical] messaging not created", async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(1); - const messagingBeforeCreationDeferred = makeDeferred(); - const { afterNextRender } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', + const messagingBeforeCreationDeferred = makeDeferred(); + const { afterNextRender } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + + // simulate messaging being created + await afterNextRender(messagingBeforeCreationDeferred.resolve); + + assert.containsOnce( + document.body, + ".o_DialogManager", + "should contain dialog manager after messaging has been created" + ); + }); + + QUnit.test("initial mount", async function (assert) { + assert.expect(1); + + await start(); + assert.containsOnce(document.body, ".o_DialogManager", "should have dialog manager"); + }); }); - - // simulate messaging being created - await afterNextRender(messagingBeforeCreationDeferred.resolve); - - assert.containsOnce( - document.body, - '.o_DialogManager', - "should contain dialog manager after messaging has been created" - ); -}); - -QUnit.test('initial mount', async function (assert) { - assert.expect(1); - - await start(); - assert.containsOnce( - document.body, - '.o_DialogManager', - "should have dialog manager" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js index 57faeaff2c7..92aa7aae24d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js @@ -5,907 +5,925 @@ import { nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_inbox_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_inbox_tests.js"); -QUnit.test('reply: discard on pressing escape', async function (assert) { - assert.expect(9); + QUnit.test("reply: discard on pressing escape", async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - // partner expected to be found by mention - pyEnv['res.partner'].create({ - email: "testpartnert@odoo.com", - name: "TestPartner", - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: 20, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, insertText, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + const pyEnv = await startServer(); + // partner expected to be found by mention + pyEnv["res.partner"].create({ + email: "testpartnert@odoo.com", + name: "TestPartner", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: 20, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, insertText, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); - await click(`.o_ComposerView_buttonEmojis`); - assert.containsOnce( - document.body, - '.o_EmojiPickerView', - "emoji list should be opened after click on emojis button" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_EmojiPickerView', - "emoji list should be closed after pressing escape on emojis button" - ); - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be opened after pressing escape on emojis button" - ); - - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "mention suggestion should be opened after typing @" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestion should be closed after pressing escape on mention suggestion" - ); - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be opened after pressing escape on mention suggestion" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after pressing escape if there was no other priority escape handler" - ); -}); - -QUnit.test('reply: discard on discard button click', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + await click(`.o_ComposerView_buttonEmojis`); + assert.containsOnce( + document.body, + ".o_EmojiPickerView", + "emoji list should be opened after click on emojis button" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonDiscard', - "composer should have a discard button" - ); - - await click(`.o_ComposerView_buttonDiscard`); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after clicking on discard" - ); -}); - -QUnit.test('reply: discard on reply button toggle', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_EmojiPickerView", + "emoji list should be closed after pressing escape on emojis button" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); - await click(`.o_MessageActionView_actionReplyTo`); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after clicking on reply button again" - ); -}); - -QUnit.test('reply: discard on click away', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be opened after pressing escape on emojis button" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "mention suggestion should be opened after typing @" + ); - document.querySelector(`.o_ComposerTextInputView_textarea`).click(); - await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be there after clicking inside itself" - ); + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be opened after pressing escape on mention suggestion" + ); - await click(`.o_ComposerView_buttonEmojis`); - assert.containsOnce( - document.body, - '.o_EmojiPickerView', - "emoji list should be opened after clicking on emojis button" - ); + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after pressing escape if there was no other priority escape handler" + ); + }); - await click(`.o_EmojiView`); - assert.containsNone( - document.body, - '.o_EmojiPickerView', - "emoji list should be closed after selecting an emoji" - ); - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)" - ); + QUnit.test("reply: discard on discard button click", async function (assert) { + assert.expect(4); - await click(`.o_MessageView`); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after clicking away" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + await click(".o_MessageView"); -QUnit.test('"reply to" composer should log note if message replied to is a note', async function (assert) { - assert.expect(6); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonDiscard", + "composer should have a discard button" + ); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - is_discussion: false, - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('/mail/message/post'); + await click(`.o_ComposerView_buttonDiscard`); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after clicking on discard" + ); + }); + + QUnit.test("reply: discard on reply button toggle", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" + ); + await click(`.o_MessageActionView_actionReplyTo`); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after clicking on reply button again" + ); + }); + + QUnit.test("reply: discard on click away", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" + ); + + document.querySelector(`.o_ComposerTextInputView_textarea`).click(); + await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be there after clicking inside itself" + ); + + await click(`.o_ComposerView_buttonEmojis`); + assert.containsOnce( + document.body, + ".o_EmojiPickerView", + "emoji list should be opened after clicking on emojis button" + ); + + await click(`.o_EmojiView`); + assert.containsNone( + document.body, + ".o_EmojiPickerView", + "emoji list should be closed after selecting an emoji" + ); + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)" + ); + + await click(`.o_MessageView`); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after clicking away" + ); + }); + + QUnit.test( + '"reply to" composer should log note if message replied to is a note', + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + is_discussion: false, + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("/mail/message/post"); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.post_data.subtype_xmlid, + "mail.mt_note", + "should set subtype_xmlid as 'note'" + ); + } + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" + document.querySelector(".o_ComposerView_buttonSend").textContent.trim(), + "Log", + "Send button text should be 'Log'" + ); + + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["/mail/message/post"]); + } + ); + + QUnit.test( + '"reply to" composer should send message if message replied to is not a note', + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + is_discussion: true, + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("/mail/message/post"); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.post_data.subtype_xmlid, + "mail.mt_comment", + "should set subtype_xmlid as 'comment'" + ); + } + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.strictEqual( + document.querySelector(".o_ComposerView_buttonSend").textContent.trim(), + "Send", + "Send button text should be 'Send'" + ); + + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["/mail/message/post"]); + } + ); + + QUnit.test("error notifications should not be shown in Inbox", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of related message + notification_status: "exception", + notification_type: "email", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + assert.containsOnce( + document.body, + ".o_MessageView_originThreadLink", + "should display origin thread link" + ); + assert.containsNone( + document.body, + ".o_MessageView_notificationIcon", + "should not display any notification icon in Inbox" + ); + }); + + QUnit.test("show subject of message in Inbox", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // not needed, for consistency + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + }); + + QUnit.test("show subject of message in history", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + history_partner_ids: [3], // not needed, for consistency + model: "mail.channel", + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + is_read: true, + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_history", + }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until history displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.history.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + }); + + QUnit.test( + "click on (non-channel/non-partner) origin thread link should redirect to form view", + async function (assert) { + assert.expect(9); + + const pyEnv = await startServer(); + const resFakeId1 = pyEnv["res.fake"].create({ name: "Some record" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.fake", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resFakeId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, env, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + patchWithCleanup(env.services.action, { + doAction(action) { + // 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 + assert.step("do-action"); + assert.strictEqual( + action.type, + "ir.actions.act_window", + "action should open a view" + ); + assert.deepEqual( + action.views, + [[false, "form"]], + "action should open form view" + ); + assert.strictEqual( + action.res_model, + "res.fake", + "action should open view with model 'res.fake' (model of message origin thread)" + ); + assert.strictEqual( + action.res_id, + resFakeId1, + "action should open view with id of resFake1 (id of message origin thread)" + ); + return Promise.resolve(); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + assert.containsOnce( + document.body, + ".o_MessageView_originThreadLink", + "should display origin thread link" ); assert.strictEqual( - args.post_data.subtype_xmlid, - "mail.mt_note", - "should set subtype_xmlid as 'note'" + document.querySelector(".o_MessageView_originThreadLink").textContent, + "Some record", + "origin thread link should display record name" + ); + + document.querySelector(".o_MessageView_originThreadLink").click(); + assert.verifySteps( + ["do-action"], + "should have made an action on click on origin thread (to open form view)" ); } - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); + ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.strictEqual( - document.querySelector('.o_ComposerView_buttonSend').textContent.trim(), - "Log", - "Send button text should be 'Log'" - ); + QUnit.test( + "subject should not be shown when subject is the same as the thread name", + async function (assert) { + assert.expect(1); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['/mail/message/post']); -}); - -QUnit.test('"reply to" composer should send message if message replied to is not a note', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - is_discussion: true, - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('/mail/message/post'); - assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" - ); - assert.strictEqual( - args.post_data.subtype_xmlid, - "mail.mt_comment", - "should set subtype_xmlid as 'comment'" + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "subject should not be shown when subject is the same as the thread name" ); } - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); + ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.strictEqual( - document.querySelector('.o_ComposerView_buttonSend').textContent.trim(), - "Send", - "Send button text should be 'Send'" - ); + QUnit.test( + "subject should not be shown when subject is the same as the thread name and both have the same prefix", + async function (assert) { + assert.expect(1); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['/mail/message/post']); -}); - -QUnit.test('error notifications should not be shown in Inbox', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of related message - notification_status: 'exception', - notification_type: 'email', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_originThreadLink', - "should display origin thread link" - ); - assert.containsNone( - document.body, - '.o_MessageView_notificationIcon', - "should not display any notification icon in Inbox" - ); -}); - -QUnit.test('show subject of message in Inbox', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // not needed, for consistency - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('show subject of message in history', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - history_partner_ids: [3], // not needed, for consistency - model: 'mail.channel', - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - is_read: true, - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_history', - }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until history displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.history.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('click on (non-channel/non-partner) origin thread link should redirect to form view', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - const resFakeId1 = pyEnv['res.fake'].create({ name: 'Some record' }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.fake', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resFakeId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, env, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - patchWithCleanup(env.services.action, { - doAction(action) { - // 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 - assert.step('do-action'); - assert.strictEqual( - action.type, - 'ir.actions.act_window', - "action should open a view" - ); - assert.deepEqual( - action.views, - [[false, 'form']], - "action should open form view" - ); - assert.strictEqual( - action.res_model, - 'res.fake', - "action should open view with model 'res.fake' (model of message origin thread)" - ); - assert.strictEqual( - action.res_id, - resFakeId1, - "action should open view with id of resFake1 (id of message origin thread)" - ); - return Promise.resolve(); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_originThreadLink', - "should display origin thread link" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_originThreadLink').textContent, - "Some record", - "origin thread link should display record name" - ); - - document.querySelector('.o_MessageView_originThreadLink').click(); - assert.verifySteps(['do-action'], "should have made an action on click on origin thread (to open form view)"); -}); - -QUnit.test('subject should not be shown when subject is the same as the thread name', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "subject should not be shown when subject is the same as the thread name" - ); -}); - -QUnit.test('subject should not be shown when subject is the same as the thread name and both have the same prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "subject should not be shown when subject is the same as the thread name and both have the same prefix" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "Re:" prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject when subject differs from thread name only by the 'Re:' prefix" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "Fw:" and "Re:" prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Fw: Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject when subject differs from thread name only by the 'Fw:' and Re:' prefix" - ); -}); - -QUnit.test('subject should be shown when the thread name has an extra prefix compared to subject', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "subject should be shown when the thread name has an extra prefix compared to subject" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "fw:" prefix and both contain another common prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "fw: re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "subject should not be shown when subject differs from thread name only by the 'fw:' prefix and both contain another common prefix" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "Re: Re:" prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Re: Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject when subject differs from thread name only by the 'Re: Re:'' prefix" - ); -}); - -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Re: Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "subject should not be shown when subject is the same as the thread name and both have the same prefix" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "Re:" prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject when subject differs from thread name only by the 'Re:' prefix" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "Fw:" and "Re:" prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Fw: Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject when subject differs from thread name only by the 'Fw:' and Re:' prefix" + ); + } + ); + + QUnit.test( + "subject should be shown when the thread name has an extra prefix compared to subject", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Re: Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "subject should be shown when the thread name has an extra prefix compared to subject" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "fw:" prefix and both contain another common prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Re: Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "fw: re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "subject should not be shown when subject differs from thread name only by the 'fw:' prefix and both contain another common prefix" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "Re: Re:" prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Re: Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject when subject differs from thread name only by the 'Re: Re:'' prefix" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js index d6dbd703a6c..51bc22d88fa 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js @@ -1,34 +1,40 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_message_edit_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_message_edit_tests.js"); -QUnit.test('click on message edit button should open edit composer', async function (assert) { - assert.expect(1); + QUnit.test( + "click on message edit button should open edit composer", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - message_type: 'comment', - model: 'mail.channel', - res_id: mailChannelId1, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_MessageView"); + await click(".o_MessageActionView_actionEdit"); + assert.containsOnce( + document.body, + ".o_MessageView_composer", + "click on message edit button should open edit composer" + ); + } + ); }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionEdit'); - assert.containsOnce(document.body, '.o_MessageView_composer', 'click on message edit button should open edit composer'); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js index 977c6c9baaf..99d630ac019 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js @@ -1,170 +1,183 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_pinned_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_pinned_tests.js"); -QUnit.test('sidebar: pinned channel 1: init with one pinned channel', async function (assert) { - assert.expect(2); + QUnit.test( + "sidebar: pinned channel 1: init with one pinned channel", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, - "The Inbox is opened in discuss" - ); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "should have the only channel of which user is member in discuss sidebar" - ); -}); - -QUnit.test('sidebar: pinned channel 2: open pinned channel', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start(); - await openDiscuss(); - - await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, - "The channel #General is displayed in discuss" - ); -}); - -QUnit.test('sidebar: pinned channel 3: open channel and leave it', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [[0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }]], - }); - const { click, openDiscuss } = await start({ - async mockRPC(route, args) { - if (args.method === 'action_unfollow') { - assert.step('action_unfollow'); - assert.deepEqual(args.args[0], [mailChannelId1], - "The right id is sent to the server to remove" + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, + "The Inbox is opened in discuss" + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "should have the only channel of which user is member in discuss sidebar" ); } - }, - }); - await openDiscuss(); + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); - assert.verifySteps([], "action_unfollow is not called yet"); + QUnit.test("sidebar: pinned channel 2: open pinned channel", async function (assert) { + assert.expect(1); - await click('.o_DiscussSidebarCategoryItem_commandLeave'); - assert.verifySteps( - [ - 'action_unfollow' - ], - "action_unfollow has been called when leaving a channel" - ); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "The channel must have been removed from discuss sidebar" - ); - assert.containsOnce( - document.body, - '.o_DiscussView_noThread', - "should have no thread opened in discuss" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start(); + await openDiscuss(); -QUnit.test('sidebar: unpin channel from bus', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, - "The Inbox is opened in discuss" - ); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "1 channel is present in discuss sidebar and it is 'general'" - ); - - await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, - "The channel #General is opened in discuss" - ); - - // Simulate receiving a leave channel notification - // (e.g. from user interaction from another device or browser tab) - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.channel/unpin', { - 'id': mailChannelId1, + await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, + "The channel #General is displayed in discuss" + ); }); + + QUnit.test("sidebar: pinned channel 3: open channel and leave it", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + const { click, openDiscuss } = await start({ + async mockRPC(route, args) { + if (args.method === "action_unfollow") { + assert.step("action_unfollow"); + assert.deepEqual( + args.args[0], + [mailChannelId1], + "The right id is sent to the server to remove" + ); + } + }, + }); + await openDiscuss(); + + await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); + assert.verifySteps([], "action_unfollow is not called yet"); + + await click(".o_DiscussSidebarCategoryItem_commandLeave"); + assert.verifySteps( + ["action_unfollow"], + "action_unfollow has been called when leaving a channel" + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "The channel must have been removed from discuss sidebar" + ); + assert.containsOnce( + document.body, + ".o_DiscussView_noThread", + "should have no thread opened in discuss" + ); + }); + + QUnit.test("sidebar: unpin channel from bus", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, + "The Inbox is opened in discuss" + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "1 channel is present in discuss sidebar and it is 'general'" + ); + + await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, + "The channel #General is opened in discuss" + ); + + // Simulate receiving a leave channel notification + // (e.g. from user interaction from another device or browser tab) + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.channel/unpin", { + id: mailChannelId1, + }); + }); + assert.containsOnce( + document.body, + ".o_DiscussView_noThread", + "should have no thread opened in discuss" + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "The channel must have been removed from discuss sidebar" + ); + }); + + QUnit.test( + "[technical] sidebar: channel group_based_subscription: mandatorily pinned", + async function (assert) { + assert.expect(2); + + // FIXME: The following is admittedly odd. + // Fixing it should entail a deeper reflexion on the group_based_subscription + // and is_pinned functionalities, especially in python. + // task-2284357 + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + group_based_subscription: true, + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "The channel #General is in discuss sidebar" + ); + assert.containsNone( + document.body, + "o_DiscussSidebarCategoryItem_commandLeave", + "The group_based_subscription channel is not unpinnable" + ); + } + ); }); - assert.containsOnce( - document.body, - '.o_DiscussView_noThread', - "should have no thread opened in discuss" - ); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "The channel must have been removed from discuss sidebar" - ); -}); - -QUnit.test('[technical] sidebar: channel group_based_subscription: mandatorily pinned', async function (assert) { - assert.expect(2); - - // FIXME: The following is admittedly odd. - // Fixing it should entail a deeper reflexion on the group_based_subscription - // and is_pinned functionalities, especially in python. - // task-2284357 - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [[0, 0, { - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }]], - group_based_subscription: true, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "The channel #General is in discuss sidebar" - ); - assert.containsNone( - document.body, - 'o_DiscussSidebarCategoryItem_commandLeave', - "The group_based_subscription channel is not unpinnable" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js index 96d4ccdcb9e..28ed7127a24 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js @@ -1,168 +1,177 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { datetime_to_str } from 'web.time'; +import { datetime_to_str } from "web.time"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_category_item_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_sidebar_category_item_tests.js"); -QUnit.test('channel - avatar: should have correct avatar', async function (assert) { - assert.expect(2); + QUnit.test("channel - avatar: should have correct avatar", async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ avatarCacheKey: '100111' }); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ avatarCacheKey: "100111" }); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - const channelItem = document.querySelector(` + const channelItem = document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - channelItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, - 1, - "channel should have an avatar" - ); + assert.strictEqual( + channelItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, + 1, + "channel should have an avatar" + ); - assert.strictEqual( - channelItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=100111`, - 'should link to the correct picture source' - ); -}); - -QUnit.test('channel - avatar: should update avatar url from bus', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ avatarCacheKey: '101010' }); - - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=101010`, - ); - - await afterNextRender(() => { - messaging.rpc({ - model: 'mail.channel', - method: 'write', - args: [[mailChannelId1], { image_128: 'This field does not matter' }], + assert.strictEqual( + channelItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, + `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=100111`, + "should link to the correct picture source" + ); }); - }); - const result = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]]); - const newCacheKey = result[0]['avatarCacheKey']; - assert.strictEqual( - document.querySelector(` + QUnit.test("channel - avatar: should update avatar url from bus", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ avatarCacheKey: "101010" }); + + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=${newCacheKey}`, - ); -}); + `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=101010` + ); -QUnit.test('chat - avatar: should have correct avatar', async function (assert) { - assert.expect(2); + await afterNextRender(() => { + messaging.rpc({ + model: "mail.channel", + method: "write", + args: [[mailChannelId1], { image_128: "This field does not matter" }], + }); + }); + const result = pyEnv["mail.channel"].searchRead([["id", "=", mailChannelId1]]); + const newCacheKey = result[0]["avatarCacheKey"]; - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo", im_status: 'offline' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] + .o_DiscussSidebarCategoryItem_image`).dataset.src, + `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=${newCacheKey}` + ); + }); - const chatItem = document.querySelector(` + QUnit.test("chat - avatar: should have correct avatar", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Demo", + im_status: "offline", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + const chatItem = document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - chatItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, - 1, - "chat should have an avatar" - ); + assert.strictEqual( + chatItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, + 1, + "chat should have an avatar" + ); - assert.strictEqual( - chatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/res.partner/${resPartnerId1}/avatar_128`, - 'should link to the partner avatar' - ); -}); - -QUnit.test('chat - sorting: should be sorted by last activity time', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [[0, 0, { - last_interest_dt: datetime_to_str(new Date(2021, 0, 1)), - partner_id: pyEnv.currentPartnerId, - }]], - channel_type: 'chat', - }, - { - channel_member_ids: [[0, 0, { - last_interest_dt: datetime_to_str(new Date(2021, 0, 2)), - partner_id: pyEnv.currentPartnerId, - }]], - channel_type: 'chat', - }, - ]); - const { click, openDiscuss } = await start(); - await openDiscuss(); - - const initialChats = document.querySelectorAll('.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item'); - assert.strictEqual( - initialChats.length, - 2, - "should have 2 livechat items" - ); - assert.strictEqual( - Number(initialChats[0].dataset.channelId), - mailChannelId2, - "first livechat should be the one with the more recent last activity time" - ); - assert.strictEqual( - Number(initialChats[1].dataset.channelId), - mailChannelId1, - "second chat should be the one with the less recent last activity time" - ); - - // post a new message on the last channel - await afterNextRender(() => initialChats[1].click()); - await afterNextRender(() => document.execCommand('insertText', false, "Blabla")); - await click('.o_ComposerView_buttonSend'); - const newChats = document.querySelectorAll('.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item'); - assert.strictEqual( - newChats.length, - 2, - "should have 2 chat items" - ); - assert.strictEqual( - Number(newChats[0].dataset.channelId), - mailChannelId1, - "first chat should be the one with the more recent last activity time" - ); - assert.strictEqual( - Number(newChats[1].dataset.channelId), - mailChannelId2, - "second chat should be the one with the less recent last activity time" - ); -}); - -}); + assert.strictEqual( + chatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, + `/web/image/res.partner/${resPartnerId1}/avatar_128`, + "should link to the partner avatar" + ); + }); + + QUnit.test( + "chat - sorting: should be sorted by last activity time", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + last_interest_dt: datetime_to_str(new Date(2021, 0, 1)), + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + last_interest_dt: datetime_to_str(new Date(2021, 0, 2)), + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + ]); + const { click, openDiscuss } = await start(); + await openDiscuss(); + + const initialChats = document.querySelectorAll( + ".o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item" + ); + assert.strictEqual(initialChats.length, 2, "should have 2 livechat items"); + assert.strictEqual( + Number(initialChats[0].dataset.channelId), + mailChannelId2, + "first livechat should be the one with the more recent last activity time" + ); + assert.strictEqual( + Number(initialChats[1].dataset.channelId), + mailChannelId1, + "second chat should be the one with the less recent last activity time" + ); + + // post a new message on the last channel + await afterNextRender(() => initialChats[1].click()); + await afterNextRender(() => document.execCommand("insertText", false, "Blabla")); + await click(".o_ComposerView_buttonSend"); + const newChats = document.querySelectorAll( + ".o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item" + ); + assert.strictEqual(newChats.length, 2, "should have 2 chat items"); + assert.strictEqual( + Number(newChats[0].dataset.channelId), + mailChannelId1, + "first chat should be the one with the more recent last activity time" + ); + assert.strictEqual( + Number(newChats[1].dataset.channelId), + mailChannelId2, + "second chat should be the one with the less recent last activity time" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js index 101bba00f60..51bbbc224ac 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js @@ -1,740 +1,865 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_category_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_sidebar_category_tests.js"); -QUnit.test('channel - counter: should not have a counter if the category is unfolded and without needaction messages', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should not have a counter if the category is unfolded and without needaction messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and without unread messages" - ); -}); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and without unread messages" + ); + } + ); -QUnit.test('channel - counter: should not have a counter if the category is unfolded and with needaction messagens', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should not have a counter if the category is unfolded and with needaction messagens", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "message 1", - model: "mail.channel", - res_id: mailChannelId1, - }, - { - body: "message_2", - model: "mail.channel", - res_id: mailChannelId2, - }, - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - { - mail_message_id: mailMessageId2, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and with needaction messages", - ); -}); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + ]); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "message 1", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + body: "message_2", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and with needaction messages" + ); + } + ); -QUnit.test('channel - counter: should not have a counter if category is folded and without needaction messages', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should not have a counter if category is folded and without needaction messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is folded and without unread messages" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is folded and without unread messages" + ); + } + ); -QUnit.test('channel - counter: should have correct value of needaction threads if category is folded and with needaction messages', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should have correct value of needaction threads if category is folded and with needaction messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "message 1", - model: "mail.channel", - res_id: mailChannelId1, - }, - { - body: "message_2", - model: "mail.channel", - res_id: mailChannelId2, - }, - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - { - mail_message_id: mailMessageId2, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - ]); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + ]); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "message 1", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + body: "message_2", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).textContent, - "2", - "should have correct value of needaction threads if category is folded and with needaction messages" - ); -}); + assert.strictEqual( + document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).textContent, + "2", + "should have correct value of needaction threads if category is folded and with needaction messages" + ); + } + ); -QUnit.test('channel - command: should have view command when category is unfolded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should have view command when category is unfolded", + async function (assert) { + assert.expect(1); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView`).length, - 1, - "should have view command when channel category is open" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView` + ).length, + 1, + "should have view command when channel category is open" + ); + } + ); -QUnit.test('channel - command: should have view command when category is folded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should have view command when category is folded", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView`).length, - 1, - "should have view command when channel category is closed" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView` + ).length, + 1, + "should have view command when channel category is closed" + ); + } + ); -QUnit.test('channel - command: should have add command when category is unfolded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should have add command when category is unfolded", + async function (assert) { + assert.expect(1); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 1, - "should have add command when channel category is open" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 1, + "should have add command when channel category is open" + ); + } + ); -QUnit.test('channel - command: should not have add command when category is folded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should not have add command when category is folded", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 0, - "should not have add command when channel category is closed" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 0, + "should not have add command when channel category is closed" + ); + } + ); -QUnit.test('channel - states: close manually by clicking the title', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - states: close manually by clicking the title", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: true, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: true, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be closed and the content should be invisible" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be closed and the content should be invisible" + ); + } + ); -QUnit.test('channel - states: open manually by clicking the title', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - states: open manually by clicking the title", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be open and the content should be visible" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be open and the content should be visible" + ); + } + ); -QUnit.test('channel - states: close should update the value on the server', async function (assert) { - assert.expect(2); + QUnit.test( + "channel - states: close should update the value on the server", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: true, - }); - const currentUserId = pyEnv.currentUserId; - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: true, + }); + const currentUserId = pyEnv.currentUserId; + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_channel_open, - true, - "the server side value should be true" - ); + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_channel_open, + true, + "the server side value should be true" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_channel_open, - false, - "the server side value should be false" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_channel_open, + false, + "the server side value should be false" + ); + } + ); -QUnit.test('channel - states: open should update the value on the server', async function (assert) { - assert.expect(2); + QUnit.test( + "channel - states: open should update the value on the server", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const currentUserId = pyEnv.currentUserId; - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const currentUserId = pyEnv.currentUserId; + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_channel_open, - false, - "the server side value should be false" - ); + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_channel_open, + false, + "the server side value should be false" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_channel_open, - true, - "the server side value should be false" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_channel_open, + true, + "the server side value should be false" + ); + } + ); -QUnit.test('channel - states: close from the bus', async function (assert) { - assert.expect(1); + QUnit.test("channel - states: close from the bus", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: true, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: true, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_channel_open': false, - }, + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_channel_open: false, + }, + }); + }); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be closed and the content should be invisible" + ); }); - }); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be closed and the content should be invisible" - ); -}); -QUnit.test('channel - states: open from the bus', async function (assert) { - assert.expect(1); + QUnit.test("channel - states: open from the bus", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_channel_open': true, - }, + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_channel_open: true, + }, + }); + }); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be open and the content should be visible" + ); }); - }); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be open and the content should be visible" - ); -}); -QUnit.test('channel - states: the active category item should be visible even if the category is closed', async function (assert) { - assert.expect(4); + QUnit.test( + "channel - states: the active category item should be visible even if the category is closed", + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); - const channel = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`); - await afterNextRender(() => { - channel.click(); - }); - assert.ok(channel.classList.contains('o-active')); + const channel = document.querySelector( + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); + await afterNextRender(() => { + channel.click(); + }); + assert.ok(channel.classList.contains("o-active")); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - 'the active channel item should remain even if the category is folded' - ); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "the active channel item should remain even if the category is folded" + ); - await click(`.o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"]`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "inactive item should be invisible if the category is folded" - ); -}); + await click( + `.o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"]` + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "inactive item should be invisible if the category is folded" + ); + } + ); -QUnit.test('chat - counter: should not have a counter if the category is unfolded and without unread messages', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should not have a counter if the category is unfolded and without unread messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and without unread messages", - ); -}); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and without unread messages" + ); + } + ); -QUnit.test('chat - counter: should not have a counter if the category is unfolded and with unread messagens', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should not have a counter if the category is unfolded and with unread messagens", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 10, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and with unread messages", - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 10, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and with unread messages" + ); + } + ); -QUnit.test('chat - counter: should not have a counter if category is folded and without unread messages', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should not have a counter if category is folded and without unread messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is folded and without unread messages" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is folded and without unread messages" + ); + } + ); -QUnit.test('chat - counter: should have correct value of unread threads if category is folded and with unread messages', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should have correct value of unread threads if category is folded and with unread messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 10, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }, - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 20, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }, - ]); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.strictEqual( - document.querySelector(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).textContent, - "2", - "should have correct value of unread threads if category is folded and with unread messages" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 10, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 20, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + ]); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.strictEqual( + document.querySelector( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).textContent, + "2", + "should have correct value of unread threads if category is folded and with unread messages" + ); + } + ); -QUnit.test('chat - command: should have add command when category is unfolded', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - command: should have add command when category is unfolded", + async function (assert) { + assert.expect(1); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 1, - "should have add command when chat category is open" - ); -}); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 1, + "should have add command when chat category is open" + ); + } + ); -QUnit.test('chat - command: should not have add command when category is folded', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - command: should not have add command when category is folded", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 0, - "should not have add command when chat category is closed" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 0, + "should not have add command when chat category is closed" + ); + } + ); -QUnit.test('chat - states: close manually by clicking the title', async function (assert) { - assert.expect(1); + QUnit.test("chat - states: close manually by clicking the title", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: true, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be closed and the content should be invisible" - ); -}); - -QUnit.test('chat - states: open manually by clicking the title', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be open and the content should be visible" - ); -}); - -QUnit.test('chat - states: close should call update server data', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: true, - }); - const currentUserId = pyEnv.currentUserId; - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_chat_open, - true, - "the value in server side should be true" - ); - - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_chat_open, - false, - "the value in server side should be false" - ); -}); - -QUnit.test('chat - states: open should call update server data', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[pyEnv.currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_chat_open, - false, - "the value in server side should be false" - ); - - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[pyEnv.currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_chat_open, - true, - "the value in server side should be true" - ); -}); - -QUnit.test('chat - states: close from the bus', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: true, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_chat_open': false, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: true, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be closed and the content should be invisible" + ); }); - }); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be close and the content should be invisible" - ); -}); -QUnit.test('chat - states: open from the bus', async function (assert) { - assert.expect(1); + QUnit.test("chat - states: open manually by clicking the title", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_chat_open': true, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be open and the content should be visible" + ); }); + + QUnit.test("chat - states: close should call update server data", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: true, + }); + const currentUserId = pyEnv.currentUserId; + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_chat_open, + true, + "the value in server side should be true" + ); + + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_chat_open, + false, + "the value in server side should be false" + ); + }); + + QUnit.test("chat - states: open should call update server data", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[pyEnv.currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_chat_open, + false, + "the value in server side should be false" + ); + + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[pyEnv.currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_chat_open, + true, + "the value in server side should be true" + ); + }); + + QUnit.test("chat - states: close from the bus", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: true, + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_chat_open: false, + }, + }); + }); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be close and the content should be invisible" + ); + }); + + QUnit.test("chat - states: open from the bus", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_chat_open: true, + }, + }); + }); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be open and the content should be visible" + ); + }); + + QUnit.test( + "chat - states: the active category item should be visible even if the category is closed", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); + + const chat = document.querySelector( + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); + await afterNextRender(() => { + chat.click(); + }); + assert.ok(chat.classList.contains("o-active")); + + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "the active chat item should remain even if the category is folded" + ); + + await click( + `.o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"]` + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "inactive item should be invisible if the category is folded" + ); + } + ); }); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be open and the content should be visible" - ); -}); - -QUnit.test('chat - states: the active category item should be visible even if the category is closed', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - const chat = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`); - await afterNextRender(() => { - chat.click(); - }); - assert.ok(chat.classList.contains('o-active')); - - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - 'the active chat item should remain even if the category is folded' - ); - - await click(`.o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"]`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "inactive item should be invisible if the category is folded" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js index 103a61feec4..d5302ff5558 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js @@ -1,133 +1,143 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { - nextAnimationFrame, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { makeDeferred } from "@mail/utils/deferred"; +import { nextAnimationFrame, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_sidebar_tests.js"); -QUnit.test('sidebar find shows channels matching search term', async function (assert) { - assert.expect(3); + QUnit.test("sidebar find shows channels matching search term", async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [], - channel_type: 'channel', - group_public_id: false, - name: 'test', - }); - const searchReadDef = makeDeferred(); - const { click, openDiscuss } = await start({ - async mockRPC(route, args) { - if (args.method === 'search_read') { - searchReadDef.resolve(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [], + channel_type: "channel", + group_public_id: false, + name: "test", + }); + const searchReadDef = makeDeferred(); + const { click, openDiscuss } = await start({ + async mockRPC(route, args) { + if (args.method === "search_read") { + searchReadDef.resolve(); + } + }, + }); + await openDiscuss(); + await click(`.o_DiscussSidebarCategory_commandAdd`); + document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); + document.execCommand("insertText", false, "test"); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + + await searchReadDef; + await nextAnimationFrame(); // ensures search_read rpc is rendered. + const results = document.querySelectorAll(".ui-autocomplete .ui-menu-item a"); + assert.ok( + results, + "should have autocomplete suggestion after typing on 'find or create channel' input" + ); + assert.strictEqual( + results.length, + // When searching for a single existing channel, the results list will have at least 2 lines: + // One for the existing channel itself + // One for creating a channel with the search term + 2 + ); + assert.strictEqual( + results[0].textContent, + "test", + "autocomplete suggestion should target the channel matching search term" + ); + }); + + QUnit.test( + "sidebar find shows channels matching search term even when user is member", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [[0, 0, { partner_id: pyEnv.currentPartnerId }]], + channel_type: "channel", + group_public_id: false, + name: "test", + }); + const searchReadDef = makeDeferred(); + const { click, openDiscuss } = await start({ + async mockRPC(route, args) { + if (args.method === "search_read") { + searchReadDef.resolve(); + } + }, + }); + await openDiscuss(); + await click(`.o_DiscussSidebarCategory_commandAdd`); + document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); + document.execCommand("insertText", false, "test"); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + + await searchReadDef; + await nextAnimationFrame(); + const results = document.querySelectorAll(".ui-autocomplete .ui-menu-item a"); + assert.ok( + results, + "should have autocomplete suggestion after typing on 'find or create channel' input" + ); + assert.strictEqual( + results.length, + // When searching for a single existing channel, the results list will have at least 2 lines: + // One for the existing channel itself + // One for creating a channel with the search term + 2 + ); + assert.strictEqual( + results[0].textContent, + "test", + "autocomplete suggestion should target the channel matching search term even if user is member" + ); } - }, - }); - await openDiscuss(); - await click(`.o_DiscussSidebarCategory_commandAdd`); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); - document.execCommand('insertText', false, "test"); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keyup')); + ); - await searchReadDef; - await nextAnimationFrame(); // ensures search_read rpc is rendered. - const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a'); - assert.ok( - results, - "should have autocomplete suggestion after typing on 'find or create channel' input" - ); - assert.strictEqual( - results.length, - // When searching for a single existing channel, the results list will have at least 2 lines: - // One for the existing channel itself - // One for creating a channel with the search term - 2 - ); - assert.strictEqual( - results[0].textContent, - "test", - "autocomplete suggestion should target the channel matching search term" - ); -}); + QUnit.test( + "sidebar channels should be ordered case insensitive alphabetically", + async function (assert) { + assert.expect(1); -QUnit.test('sidebar find shows channels matching search term even when user is member', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - ], - channel_type: 'channel', - group_public_id: false, - name: 'test', - }); - const searchReadDef = makeDeferred(); - const { click, openDiscuss } = await start({ - async mockRPC(route, args) { - if (args.method === 'search_read') { - searchReadDef.resolve(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { name: "Xyz" }, + { name: "abc" }, + { name: "Abc" }, + { name: "Xyz" }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + const results = document.querySelectorAll( + ".o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategoryItem_name" + ); + assert.deepEqual( + [ + results[0].textContent, + results[1].textContent, + results[2].textContent, + results[3].textContent, + ], + ["abc", "Abc", "Xyz", "Xyz"], + "Channel name should be in case insensitive alphabetical order" + ); } - }, + ); }); - await openDiscuss(); - await click(`.o_DiscussSidebarCategory_commandAdd`); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); - document.execCommand('insertText', false, "test"); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keyup')); - - await searchReadDef; - await nextAnimationFrame(); - const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a'); - assert.ok( - results, - "should have autocomplete suggestion after typing on 'find or create channel' input" - ); - assert.strictEqual( - results.length, - // When searching for a single existing channel, the results list will have at least 2 lines: - // One for the existing channel itself - // One for creating a channel with the search term - 2 - ); - assert.strictEqual( - results[0].textContent, - "test", - "autocomplete suggestion should target the channel matching search term even if user is member" - ); -}); - -QUnit.test('sidebar channels should be ordered case insensitive alphabetically', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { name: "Xyz" }, - { name: "abc" }, - { name: "Abc" }, - { name: "Xyz" }, - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - const results = document.querySelectorAll('.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategoryItem_name'); - assert.deepEqual( - [results[0].textContent, results[1].textContent, results[2].textContent, results[3].textContent], - ["abc", "Abc", "Xyz", "Xyz"], - "Channel name should be in case insensitive alphabetical order" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js index 8ea07247586..1e5d837ed71 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js @@ -6,3804 +6,4008 @@ import { nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; -import { destroy } from '@web/../tests/helpers/utils'; +import { destroy } from "@web/../tests/helpers/utils"; -import { makeTestPromise, file } from 'web.test_utils'; +import { makeTestPromise, file } from "web.test_utils"; -import { makeFakePresenceService } from '@bus/../tests/helpers/mock_services'; +import { makeFakePresenceService } from "@bus/../tests/helpers/mock_services"; const { createFile, inputFiles } = file; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_tests.js"); -QUnit.test('messaging not created', async function (assert) { - assert.expect(1); + QUnit.test("messaging not created", async function (assert) { + assert.expect(1); - const messagingBeforeCreationDeferred = makeTestPromise(); - const { openDiscuss } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none' - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsOnce(document.body, '.o_DiscussContainer_spinner', "should display messaging not initialized"); - messagingBeforeCreationDeferred.resolve(); -}); + const messagingBeforeCreationDeferred = makeTestPromise(); + const { openDiscuss } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsOnce( + document.body, + ".o_DiscussContainer_spinner", + "should display messaging not initialized" + ); + messagingBeforeCreationDeferred.resolve(); + }); -QUnit.test('discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards', async function (assert) { - assert.expect(1); + QUnit.test( + "discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards", + async function (assert) { + assert.expect(1); - const messagingBeforeCreationDeferred = makeTestPromise(); - const { env, openDiscuss } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); + const messagingBeforeCreationDeferred = makeTestPromise(); + const { env, openDiscuss } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); - await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); - const { messaging } = env.services.messaging.modelManager; - assert.ok( - messaging.discuss.discussView, - "discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards" - ); -}); - -QUnit.test('discuss should be marked as closed when the component is unmounted', async function (assert) { - assert.expect(1); - - const { messaging, openDiscuss, webClient } = await start(); - await openDiscuss(); - - await afterNextRender(() => destroy(webClient)); - assert.notOk( - messaging.discuss.discussView, - "discuss should be marked as closed when the component is unmounted" - ); -}); - -QUnit.test('messaging not initialized', async function (assert) { - assert.expect(1); - - const messaginginitializedDeferred = makeTestPromise(); - const { openDiscuss } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized + await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); + const { messaging } = env.services.messaging.modelManager; + assert.ok( + messaging.discuss.discussView, + "discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards" + ); } - }, - waitUntilMessagingCondition: 'created', - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.strictEqual( - document.querySelectorAll('.o_DiscussContainer_spinner').length, - 1, - "should display messaging not initialized" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); + ); -QUnit.test('messaging becomes initialized', async function (assert) { - assert.expect(2); + QUnit.test( + "discuss should be marked as closed when the component is unmounted", + async function (assert) { + assert.expect(1); - const messagingInitializedProm = makeTestPromise(); + const { messaging, openDiscuss, webClient } = await start(); + await openDiscuss(); - const { openDiscuss } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messagingInitializedProm; + await afterNextRender(() => destroy(webClient)); + assert.notOk( + messaging.discuss.discussView, + "discuss should be marked as closed when the component is unmounted" + ); } - }, - waitUntilMessagingCondition: 'created', - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.strictEqual( - document.querySelectorAll('.o_DiscussContainer_spinner').length, - 1, - "should display messaging not initialized" - ); + ); - await afterNextRender(() => messagingInitializedProm.resolve()); - assert.strictEqual( - document.querySelectorAll('.o_DiscussContainer_spinner').length, - 0, - "should no longer display messaging not initialized" - ); -}); + QUnit.test("messaging not initialized", async function (assert) { + assert.expect(1); -QUnit.test('basic rendering', async function (assert) { - assert.expect(4); + const messaginginitializedDeferred = makeTestPromise(); + const { openDiscuss } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + waitUntilMessagingCondition: "created", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.strictEqual( + document.querySelectorAll(".o_DiscussContainer_spinner").length, + 1, + "should display messaging not initialized" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_DiscussView_sidebar').length, - 1, - "should have a sidebar section" - ); - assert.strictEqual( - document.querySelectorAll('.o_DiscussView_content').length, - 1, - "should have content section" - ); - assert.strictEqual( - document.querySelectorAll('.o_DiscussView_thread').length, - 1, - "should have thread section inside content" - ); - assert.ok( - document.querySelector('.o_DiscussView_thread').classList.contains('o_ThreadView'), - "thread section should use ThreadView component" - ); -}); + QUnit.test("messaging becomes initialized", async function (assert) { + assert.expect(2); -QUnit.test('basic rendering: sidebar', async function (assert) { - assert.expect(18); + const messagingInitializedProm = makeTestPromise(); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_category`).length, - 3, - "should have 3 groups in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryMailbox`).length, - 1, - "should have group 'Mailbox' in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + const { openDiscuss } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messagingInitializedProm; + } + }, + waitUntilMessagingCondition: "created", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.strictEqual( + document.querySelectorAll(".o_DiscussContainer_spinner").length, + 1, + "should display messaging not initialized" + ); + + await afterNextRender(() => messagingInitializedProm.resolve()); + assert.strictEqual( + document.querySelectorAll(".o_DiscussContainer_spinner").length, + 0, + "should no longer display messaging not initialized" + ); + }); + + QUnit.test("basic rendering", async function (assert) { + assert.expect(4); + + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(".o_DiscussView_sidebar").length, + 1, + "should have a sidebar section" + ); + assert.strictEqual( + document.querySelectorAll(".o_DiscussView_content").length, + 1, + "should have content section" + ); + assert.strictEqual( + document.querySelectorAll(".o_DiscussView_thread").length, + 1, + "should have thread section inside content" + ); + assert.ok( + document.querySelector(".o_DiscussView_thread").classList.contains("o_ThreadView"), + "thread section should use ThreadView component" + ); + }); + + QUnit.test("basic rendering: sidebar", async function (assert) { + assert.expect(18); + + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_category`).length, + 3, + "should have 3 groups in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_categoryMailbox`).length, + 1, + "should have group 'Mailbox' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox .o_DiscussSidebarMailboxView `).length, - 3, - "should have 3 mailbox items" - ); - assert.strictEqual( - document.querySelectorAll(` + 3, + "should have 3 mailbox items" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `).length, - 1, - "should have inbox mailbox item" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have inbox mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `).length, - 1, - "should have starred mailbox item" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have starred mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `).length, - 1, - "should have history mailbox item" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_sidebar .o_DiscussSidebarView_separator`).length, - 2, - "should have 2 separators (separating 'Start a meeting' button, mailboxes and channels, but that's not tested)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel`).length, - 1, - "should have group 'Channel' in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have history mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_sidebar .o_DiscussSidebarView_separator`) + .length, + 2, + "should have 2 separators (separating 'Start a meeting' button, mailboxes and channels, but that's not tested)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel`).length, + 1, + "should have group 'Channel' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header `).length, - 1, - "should have header in channel group" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have header in channel group" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_titleText - `).textContent.trim(), - "Channels" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_content`).length, - 1, - "channel category should list items" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 0, - "channel category should have no item by default" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat`).length, - 1, - "should have group 'Chat' in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header`).length, - 1, - "channel category should have a header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`).length, - 1, - "should have title in chat header" - ); - assert.strictEqual( - document.querySelector(` + ` + ) + .textContent.trim(), + "Channels" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_content` + ).length, + 1, + "channel category should list items" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 0, + "channel category should have no item by default" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_categoryChat`).length, + 1, + "should have group 'Chat' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header` + ).length, + 1, + "channel category should have a header" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title` + ).length, + 1, + "should have title in chat header" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title .o_DiscussSidebarCategory_titleText - `).textContent.trim(), - "Direct Messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_content`).length, - 1, - "chat category should list items" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`).length, - 0, - "chat category should have no item by default" - ); -}); + ` + ) + .textContent.trim(), + "Direct Messages" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_content` + ).length, + 1, + "chat category should list items" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ).length, + 0, + "chat category should have no item by default" + ); + }); -QUnit.test('sidebar: basic mailbox rendering', async function (assert) { - assert.expect(5); + QUnit.test("sidebar: basic mailbox rendering", async function (assert) { + assert.expect(5); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - const inbox = document.querySelector(` + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + const inbox = document.querySelector(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.strictEqual( - inbox.querySelectorAll(`:scope .o_ThreadIconView`).length, - 1, - "mailbox should have an icon" - ); - assert.strictEqual( - inbox.querySelectorAll(`:scope .o_ThreadIconView_mailboxInbox`).length, - 1, - "inbox should have 'inbox' icon" - ); - assert.strictEqual( - inbox.querySelectorAll(`:scope .o_DiscussSidebarMailboxView_name`).length, - 1, - "mailbox should have a name" - ); - assert.strictEqual( - inbox.querySelector(`:scope .o_DiscussSidebarMailboxView_name`).textContent, - "Inbox", - "inbox should have name 'Inbox'" - ); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_ThreadIconView`).length, + 1, + "mailbox should have an icon" + ); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_ThreadIconView_mailboxInbox`).length, + 1, + "inbox should have 'inbox' icon" + ); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_DiscussSidebarMailboxView_name`).length, + 1, + "mailbox should have a name" + ); + assert.strictEqual( + inbox.querySelector(`:scope .o_DiscussSidebarMailboxView_name`).textContent, + "Inbox", + "inbox should have name 'Inbox'" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "should have no counter when equal to 0 (default value)" - ); -}); + 0, + "should have no counter when equal to 0 (default value)" + ); + }); -QUnit.test('sidebar: default active inbox', async function (assert) { - assert.expect(1); + QUnit.test("sidebar: default active inbox", async function (assert) { + assert.expect(1); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - const inbox = document.querySelector(` + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + const inbox = document.querySelector(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.ok( - inbox.classList.contains('o-active'), - "inbox should be active by default" - ); -}); + assert.ok(inbox.classList.contains("o-active"), "inbox should be active by default"); + }); -QUnit.test('sidebar: change item', async function (assert) { - assert.expect(4); + QUnit.test("sidebar: change item", async function (assert) { + assert.expect(4); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox should be active by default" - ); - assert.notOk( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred should be inactive by default" - ); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox should be active by default" + ); + assert.notOk( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred should be inactive by default" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `); - assert.notOk( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should become inactive" - ); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should become active"); -}); + assert.notOk( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should become inactive" + ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should become active" + ); + }); -QUnit.test('sidebar: inbox with counter', async function (assert) { - assert.expect(2); + QUnit.test("sidebar: inbox with counter", async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - pyEnv['mail.notification'].create({ notification_type: 'inbox', res_partner_id: pyEnv.currentPartnerId }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const pyEnv = await startServer(); + pyEnv["mail.notification"].create({ + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 1, - "should display a counter (= have a counter when different from 0)" - ); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + 1, + "should display a counter (= have a counter when different from 0)" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "1", - "should have counter value" - ); -}); + "1", + "should have counter value" + ); + }); -QUnit.test('sidebar: add channel', async function (assert) { - assert.expect(3); + QUnit.test("sidebar: add channel", async function (assert) { + assert.expect(3); - const { click, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + const { click, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd `).length, - 1, - "should be able to add channel from header" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should be able to add channel from header" + ); + assert.strictEqual( + document.querySelector(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd `).title, - "Add or join a channel"); + "Add or join a channel" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd`); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_addingItemInput`).length, - 1, - "should have item to add a new channel" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd` + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_addingItemInput` + ).length, + 1, + "should have item to add a new channel" + ); + }); -QUnit.test('sidebar: basic channel rendering', async function (assert) { - assert.expect(12); + QUnit.test("sidebar: basic channel rendering", async function (assert) { + assert.expect(12); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 1, - "should have one channel item"); - let channel = document.querySelector(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have one channel item" + ); + let channel = document.querySelector(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - assert.ok( - channel, - 'channel 1 should be in sidebar' - ); - assert.notOk( - channel.classList.contains('o-active'), - "should not be active by default" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, - 1, - "should have a name" - ); - assert.strictEqual( - channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, - "General", - "should have name value" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, - 1, - "should have commands" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 2, - "should have 2 commands" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`).length, - 1, - "should have 'settings' command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`).length, - 1, - "should have 'leave' command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 0, - "should have a counter when equals 0 (default value)" - ); + assert.ok(channel, "channel 1 should be in sidebar"); + assert.notOk(channel.classList.contains("o-active"), "should not be active by default"); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, + 1, + "should have a name" + ); + assert.strictEqual( + channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, + "General", + "should have name value" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, + 1, + "should have commands" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 2, + "should have 2 commands" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`) + .length, + 1, + "should have 'settings' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`) + .length, + 1, + "should have 'leave' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 0, + "should have a counter when equals 0 (default value)" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); - channel = document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); - assert.hasClass( - channel, - 'o-active', - "channel should become active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_composer`).length, - 1, - "should have composer section inside thread content (can post message in channel)" - ); -}); + await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); + channel = document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ); + assert.hasClass(channel, "o-active", "channel should become active"); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_composer`).length, + 1, + "should have composer section inside thread content (can post message in channel)" + ); + }); -QUnit.test('sidebar: channel rendering with needaction counter', async function (assert) { - assert.expect(5); + QUnit.test("sidebar: channel rendering with needaction counter", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1 - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); - const { openDiscuss } = await start(); - await openDiscuss(); - const channel = document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 1, - "should have a counter when different from 0" - ); - assert.strictEqual( - channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, - "1", - "should have counter value" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 1, - "should have single command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`).length, - 1, - "should have 'settings' command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`).length, - 0, - "should not have 'leave' command" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + const { openDiscuss } = await start(); + await openDiscuss(); + const channel = document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 1, + "should have a counter when different from 0" + ); + assert.strictEqual( + channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, + "1", + "should have counter value" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 1, + "should have single command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`) + .length, + 1, + "should have 'settings' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`) + .length, + 0, + "should not have 'leave' command" + ); + }); -QUnit.test('sidebar: public channel rendering', async function (assert) { - assert.expect(3); + QUnit.test("sidebar: public channel rendering", async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create([ - { name: "channel1", channel_type: 'channel', group_public_id: false}, - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 1, - "should have 1 channel items" - ); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create([ + { name: "channel1", channel_type: "channel", group_public_id: false }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have 1 channel items" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have channel 1" - ); - const channel1 = document.querySelector(` + 1, + "should have channel 1" + ); + const channel1 = document.querySelector(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - assert.ok( - channel1.querySelectorAll(`:scope .o_ThreadIconView_publicChannel`).length, - "channel1 (public) should have globe icon" - ); -}); + assert.ok( + channel1.querySelectorAll(`:scope .o_ThreadIconView_publicChannel`).length, + "channel1 (public) should have globe icon" + ); + }); -QUnit.test('sidebar: basic chat rendering', async function (assert) { - assert.expect(9); + QUnit.test("sidebar: basic chat rendering", async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', // testing a chat is the goal of the test - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`).length, - 1, - "should have one chat item" - ); - const chat = document.querySelector(` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", // testing a chat is the goal of the test + }); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have one chat item" + ); + const chat = document.querySelector(` .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - assert.ok(chat, "should have channel 1 in the sidebar"); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_ThreadIconView`).length, - 1, - "should have an icon" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, - 1, - "should have a name" - ); - assert.strictEqual( - chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, - "Demo", - "should have correspondent name as name" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, - 1, - "should have commands" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 1, - "should have 1 command" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, - 1, - "should have 'unpin' command" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 0, - "should have a counter when equals 0 (default value)" - ); -}); + assert.ok(chat, "should have channel 1 in the sidebar"); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_ThreadIconView`).length, + 1, + "should have an icon" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, + 1, + "should have a name" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, + "Demo", + "should have correspondent name as name" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, + 1, + "should have commands" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 1, + "should have 1 command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, + 1, + "should have 'unpin' command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 0, + "should have a counter when equals 0 (default value)" + ); + }); -QUnit.test('sidebar: chat rendering with unread counter', async function (assert) { - assert.expect(4); + QUnit.test("sidebar: chat rendering with unread counter", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 100, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); - const chat = document.querySelector(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 1, - "should have a counter when different from 0" - ); - assert.strictEqual( - chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, - "100", - "should have counter value" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 0, - "should have no command" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, - 0, - "should not have 'unpin' command" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 100, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + const chat = document.querySelector( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 1, + "should have a counter when different from 0" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, + "100", + "should have counter value" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 0, + "should have no command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, + 0, + "should not have 'unpin' command" + ); + }); -QUnit.test('sidebar: chat im_status rendering', async function (assert) { - assert.expect(7); + QUnit.test("sidebar: chat im_status rendering", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { im_status: 'offline', name: "Partner1" }, - { im_status: 'online', name: "Partner2" }, - { im_status: 'away', name: "Partner3" }, - ]); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId3 }], - ], - channel_type: 'chat', - } - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`).length, - 3, - "should have 3 chat items" - ); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { im_status: "offline", name: "Partner1" }, + { im_status: "online", name: "Partner2" }, + { im_status: "away", name: "Partner3" }, + ]); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId3 }], + ], + channel_type: "chat", + }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ).length, + 3, + "should have 3 chat items" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have Partner 1" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have Partner 1" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have Partner 2" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have Partner 2" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId3}"] `).length, - 1, - "should have Partner 3" - ); - const chat1 = document.querySelector(` + 1, + "should have Partner 3" + ); + const chat1 = document.querySelector(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - const chat2 = document.querySelector(` + const chat2 = document.querySelector(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] `); - const chat3 = document.querySelector(` + const chat3 = document.querySelector(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId3}"] `); - assert.strictEqual( - chat1.querySelectorAll(`:scope .o_ThreadIconView_offline`).length, - 1, - "chat1 should have offline icon" - ); - assert.strictEqual( - chat2.querySelectorAll(`:scope .o_ThreadIconView_online`).length, - 1, - "chat2 should have online icon" - ); - assert.strictEqual( - chat3.querySelectorAll(`:scope .o_ThreadIconView_away`).length, - 1, - "chat3 should have away icon" - ); -}); + assert.strictEqual( + chat1.querySelectorAll(`:scope .o_ThreadIconView_offline`).length, + 1, + "chat1 should have offline icon" + ); + assert.strictEqual( + chat2.querySelectorAll(`:scope .o_ThreadIconView_online`).length, + 1, + "chat2 should have online icon" + ); + assert.strictEqual( + chat3.querySelectorAll(`:scope .o_ThreadIconView_away`).length, + 1, + "chat3 should have away icon" + ); + }); -QUnit.test('sidebar: chat custom name', async function (assert) { - assert.expect(1); + QUnit.test("sidebar: chat custom name", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Marc Demo" }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - custom_channel_name: "Marc", - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); - const chat = document.querySelector(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`); - assert.strictEqual( - chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, - "Marc", - "chat should have custom name as name" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Marc Demo" }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + custom_channel_name: "Marc", + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + const chat = document.querySelector( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, + "Marc", + "chat should have custom name as name" + ); + }); -QUnit.test('default thread rendering', async function (assert) { - assert.expect(16); + QUnit.test("default thread rendering", async function (assert) { + assert.expect(16); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `).length, - 1, - "should have inbox mailbox in the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + 1, + "should have inbox mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `).length, - 1, - "should have starred mailbox in the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + 1, + "should have starred mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `).length, - 1, - "should have history mailbox in the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have history mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have channel 1 in the sidebar" - ); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have channel 1 in the sidebar" + ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in inbox" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have empty thread in inbox" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty - `).textContent.trim(), - "Congratulations, your inbox is empty New messages appear here." - ); + ` + ) + .textContent.trim(), + "Congratulations, your inbox is empty New messages appear here." + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in starred" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty - `).textContent.trim(), - "No starred messages You can mark any message as 'starred', and it shows up in this mailbox." - ); + ` + ) + .textContent.trim(), + "No starred messages You can mark any message as 'starred', and it shows up in this mailbox." + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).classList.contains('o-active'), - "history mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .classList.contains("o-active"), + "history mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in starred" - ); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_MessageListView_empty`).textContent.trim(), - "No history messages Messages marked as read will appear in the history." - ); + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document + .querySelector(`.o_DiscussView_thread .o_MessageListView_empty`) + .textContent.trim(), + "No history messages Messages marked as read will appear in the history." + ); - await click(` + await click(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.ok( - document.querySelector(` + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `).classList.contains('o-active'), - "channel 20 should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "channel 20 should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in starred" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty - `).textContent.trim(), - "There are no messages in this conversation." - ); -}); + ` + ) + .textContent.trim(), + "There are no messages in this conversation." + ); + }); -QUnit.test('initially load messages from inbox', async function (assert) { - assert.expect(3); + QUnit.test("initially load messages from inbox", async function (assert) { + assert.expect(3); - const { openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/inbox/messages') { - assert.step('/mail/channel/messages'); - assert.strictEqual( - args.limit, - 30, - "should fetch up to 30 messages" - ); - } - }, - }); - await openDiscuss(); - assert.verifySteps(['/mail/channel/messages']); -}); + const { openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/inbox/messages") { + assert.step("/mail/channel/messages"); + assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + } + }, + }); + await openDiscuss(); + assert.verifySteps(["/mail/channel/messages"]); + }); -QUnit.test('default select thread in discuss params', async function (assert) { - assert.expect(1); + QUnit.test("default select thread in discuss params", async function (assert) { + assert.expect(1); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_starred', - }, - } - }); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should become active" - ); -}); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_starred", + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should become active" + ); + }); -QUnit.test('auto-select thread in discuss context', async function (assert) { - assert.expect(1); + QUnit.test("auto-select thread in discuss context", async function (assert) { + assert.expect(1); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { - active_id: 'mail.box_starred', - }, - }, - }); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should become active" - ); -}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { + active_id: "mail.box_starred", + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should become active" + ); + }); -QUnit.test('load single message from channel initially', async function (assert) { - assert.expect(6); + QUnit.test("load single message from channel initially", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1 - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - assert.strictEqual( - args.limit, - 30, - "should fetch up to 30 messages" - ); - } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_messageList`).length, - 1, - "should have list of messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_separatorDate`).length, - 1, - "should have a single date separator" // to check: may be client timezone dependent - ); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_MessageListView_separatorLabelDate`).textContent, - "April 20, 2019", - "should display date day of messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`).length, - 1, - "should have a single message" - ); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_messageList`).length, + 1, + "should have list of messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_separatorDate`) + .length, + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector( + `.o_DiscussView_thread .o_MessageListView_separatorLabelDate` + ).textContent, + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`) + .length, + 1, + "should have a single message" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_MessageListView_message[data-message-id="${mailMessageId1}"] `).length, - 1, - "should have message with Id 100" - ); -}); + 1, + "should have message with Id 100" + ); + }); -QUnit.test('open channel from active_id as channel id', async function (assert) { - assert.expect(1); + QUnit.test("open channel from active_id as channel id", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - } - }); - await openDiscuss(); - assert.containsOnce( - document.body, - ` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ` .o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `, - "should have channel 1 open in Discuss when providing its active_id" - ); -}); - -QUnit.test('basic rendering of message', async function (assert) { - // AKU TODO: should be in message-only tests - assert.expect(15); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "body
", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1 - }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - const message = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId1}"] - `); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_sidebar`).length, - 1, - "should have message sidebar of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, - 1, - "should have author avatar in sidebar of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, - `/mail/channel/${mailChannelId1}/partner/${resPartnerId1}/avatar_128`, - "should have url of message in author avatar sidebar" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_core`).length, - 1, - "should have core part of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_header`).length, - 1, - "should have header in core part of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_authorName`).length, - 1, - "should have author name in header of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_authorName`).textContent, - "Demo", - "should have textually author name in header of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_header .o_MessageView_date`).length, - 1, - "should have date in header of message" - ); - - await click('.o_MessageView'); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionList`).length, - 1, - "should action list in message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView`).length, - 3, - "should have 3 actions in action list of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "should have action to star message" - ); - assert.containsOnce( - document.body, - '.o_MessageActionView_actionReaction', - "should have action to add a reaction" - ); - assert.containsOnce( - message, - '.o_MessageActionView_actionReplyTo', - "should have action to reply to message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_content`).length, - 1, - "should have content in core part of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_content`).textContent.trim(), - "body", - "should have body of message in content part of message" - ); -}); - -QUnit.test('should not be able to reply to temporary/transient messages', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - // these user interactions is to forge a transient message response from channel command "/who" - await insertText('.o_ComposerTextInputView_textarea', "/who"); - await click('.o_ComposerView_buttonSend'); - // click on message to show actions on the transient message resulting from the "/who" command - await click('.o_MessageView'); - assert.containsNone( - document.body, - '.o_MessageActionView_actionReplyTo', - "should not have action to reply to temporary/transient messages" - ); -}); - -QUnit.test('basic rendering of squashed message', async function (assert) { - // messages are squashed when "close", e.g. less than 1 minute has elapsed - // from messages of same author and same thread. Note that this should - // be working in non-mailboxes - // AKU TODO: should be message and/or message list-only tests - assert.expect(12); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - author_id: resPartnerId1, // must be same author as other message - body: "body1
", // random body, set for consistency - date: "2019-04-20 10:00:00", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type- - model: 'mail.channel', // to link message to channel - res_id: mailChannelId1, // id of related channel - }, - { - author_id: resPartnerId1, // must be same author as other message - body: "body2
", // random body, will be asserted in the test - date: "2019-04-20 10:00:30", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type - model: 'mail.channel', // to link message to channel - res_id: mailChannelId1, // id of related channel - } - ]); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message - `).length, - 2, - "should have 2 messages" - ); - const message1 = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId1}"] - `); - const message2 = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId2}"] - `); - assert.notOk( - message1.classList.contains('o-squashed'), - "message 1 should not be squashed" - ); - assert.notOk( - message1.querySelector(`:scope .o_MessageView_sidebar`).classList.contains('o-message-squashed'), - "message 1 should not have squashed sidebar" - ); - assert.ok( - message2.classList.contains('o-squashed'), - "message 2 should be squashed" - ); - assert.ok( - message2.querySelector(`:scope .o_MessageView_sidebar`).classList.contains('o-message-squashed'), - "message 2 should have squashed sidebar" - ); - - await click('.o_MessageView.o-squashed'); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_sidebar .o_MessageView_date`).length, - 1, - "message 2 should have date in sidebar" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageActionList`).length, - 1, - "message 2 should have some actions" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "message 2 should have star action in action list" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_core`).length, - 1, - "message 2 should have core part" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_header`).length, - 0, - "message 2 should have a header in core part" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_content`).length, - 1, - "message 2 should have some content in core part" - ); - assert.strictEqual( - message2.querySelector(`:scope .o_MessageView_content`).textContent.trim(), - "body2", - "message 2 should have body in content part" - ); -}); - -QUnit.test('inbox messages are never squashed', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - author_id: resPartnerId1, // must be same author as other message - body: "body1
", // random body, set for consistency - date: "2019-04-20 10:00:00", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type- - model: 'mail.channel', // to link message to channel - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency - res_id: mailChannelId1, // id of related channel - }, - { - author_id: resPartnerId1, // must be same author as other message - body: "body2
", // random body, will be asserted in the test - date: "2019-04-20 10:00:30", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type - model: 'mail.channel', // to link message to channel - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency - res_id: mailChannelId1, // id of related channel - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, { - mail_message_id: mailMessageId2, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - ]); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + "should have channel 1 open in Discuss when providing its active_id" ); - }, - }); + }); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message - `).length, - 2, - "should have 2 messages" - ); - const message1 = document.querySelector(` + QUnit.test("basic rendering of message", async function (assert) { + // AKU TODO: should be in message-only tests + assert.expect(15); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "body
", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + const message = document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message[data-message-id="${mailMessageId1}"] `); - const message2 = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId2}"] - `); - assert.notOk( - message1.classList.contains('o-squashed'), - "message 1 should not be squashed" - ); - assert.notOk( - message2.classList.contains('o-squashed'), - "message 2 should not be squashed" - ); -}); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_sidebar`).length, + 1, + "should have message sidebar of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, + 1, + "should have author avatar in sidebar of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, + `/mail/channel/${mailChannelId1}/partner/${resPartnerId1}/avatar_128`, + "should have url of message in author avatar sidebar" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_core`).length, + 1, + "should have core part of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_header`).length, + 1, + "should have header in core part of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_authorName`).length, + 1, + "should have author name in header of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_authorName`).textContent, + "Demo", + "should have textually author name in header of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_header .o_MessageView_date`).length, + 1, + "should have date in header of message" + ); -QUnit.test('load all messages from channel initially, less than fetch limit (29 < 30)', async function (assert) { - // AKU TODO: thread specific test - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['res.partner'].create({}); - for (let i = 28; i >= 0; i--) { - pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "not empty", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1, + await click(".o_MessageView"); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionList`).length, + 1, + "should action list in message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView`).length, + 3, + "should have 3 actions in action list of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "should have action to star message" + ); + assert.containsOnce( + document.body, + ".o_MessageActionView_actionReaction", + "should have action to add a reaction" + ); + assert.containsOnce( + message, + ".o_MessageActionView_actionReplyTo", + "should have action to reply to message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_content`).length, + 1, + "should have content in core part of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_content`).textContent.trim(), + "body", + "should have body of message in content part of message" + ); }); - } - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + + QUnit.test( + "should not be able to reply to temporary/transient messages", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + // these user interactions is to forge a transient message response from channel command "/who" + await insertText(".o_ComposerTextInputView_textarea", "/who"); + await click(".o_ComposerView_buttonSend"); + // click on message to show actions on the transient message resulting from the "/who" command + await click(".o_MessageView"); + assert.containsNone( + document.body, + ".o_MessageActionView_actionReplyTo", + "should not have action to reply to temporary/transient messages" + ); } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + ); + + QUnit.test("basic rendering of squashed message", async function (assert) { + // messages are squashed when "close", e.g. less than 1 minute has elapsed + // from messages of same author and same thread. Note that this should + // be working in non-mailboxes + // AKU TODO: should be message and/or message list-only tests + assert.expect(12); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + author_id: resPartnerId1, // must be same author as other message + body: "body1
", // random body, set for consistency + date: "2019-04-20 10:00:00", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type- + model: "mail.channel", // to link message to channel + res_id: mailChannelId1, // id of related channel + }, + { + author_id: resPartnerId1, // must be same author as other message + body: "body2
", // random body, will be asserted in the test + date: "2019-04-20 10:00:30", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type + model: "mail.channel", // to link message to channel + res_id: mailChannelId1, // id of related channel + }, + ]); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message + `).length, + 2, + "should have 2 messages" + ); + const message1 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId1}"] + `); + const message2 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId2}"] + `); + assert.notOk( + message1.classList.contains("o-squashed"), + "message 1 should not be squashed" + ); + assert.notOk( + message1 + .querySelector(`:scope .o_MessageView_sidebar`) + .classList.contains("o-message-squashed"), + "message 1 should not have squashed sidebar" + ); + assert.ok(message2.classList.contains("o-squashed"), "message 2 should be squashed"); + assert.ok( + message2 + .querySelector(`:scope .o_MessageView_sidebar`) + .classList.contains("o-message-squashed"), + "message 2 should have squashed sidebar" + ); + + await click(".o_MessageView.o-squashed"); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_sidebar .o_MessageView_date`) + .length, + 1, + "message 2 should have date in sidebar" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageActionList`).length, + 1, + "message 2 should have some actions" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "message 2 should have star action in action list" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_core`).length, + 1, + "message 2 should have core part" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_header`).length, + 0, + "message 2 should have a header in core part" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_content`).length, + 1, + "message 2 should have some content in core part" + ); + assert.strictEqual( + message2.querySelector(`:scope .o_MessageView_content`).textContent.trim(), + "body2", + "message 2 should have body in content part" + ); + }); + + QUnit.test("inbox messages are never squashed", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + author_id: resPartnerId1, // must be same author as other message + body: "body1
", // random body, set for consistency + date: "2019-04-20 10:00:00", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type- + model: "mail.channel", // to link message to channel + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency + res_id: mailChannelId1, // id of related channel + }, + { + author_id: resPartnerId1, // must be same author as other message + body: "body2
", // random body, will be asserted in the test + date: "2019-04-20 10:00:30", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type + model: "mail.channel", // to link message to channel + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency + res_id: mailChannelId1, // id of related channel + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message + `).length, + 2, + "should have 2 messages" + ); + const message1 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId1}"] + `); + const message2 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId2}"] + `); + assert.notOk( + message1.classList.contains("o-squashed"), + "message 1 should not be squashed" + ); + assert.notOk( + message2.classList.contains("o-squashed"), + "message 2 should not be squashed" + ); + }); + + QUnit.test( + "load all messages from channel initially, less than fetch limit (29 < 30)", + async function (assert) { + // AKU TODO: thread specific test + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["res.partner"].create({}); + for (let i = 28; i >= 0; i--) { + pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorDate `).length, - 1, - "should have a single date separator" // to check: may be client timezone dependent - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorLabelDate `).textContent, - "April 20, 2019", - "should display date day of messages" - ); - assert.strictEqual( - document.querySelectorAll(` + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 29, - "should have 29 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 29, + "should have 29 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 0, - "should not have load more link" - ); -}); + 0, + "should not have load more link" + ); + } + ); -QUnit.test('load more messages from channel', async function (assert) { - // AKU: thread specific test - assert.expect(6); + QUnit.test("load more messages from channel", async function (assert) { + // AKU: thread specific test + assert.expect(6); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['res.partner'].create({}); - for (let i = 0; i < 40; i++) { - pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "not empty", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1, - }); - } - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["res.partner"].create({}); + for (let i = 0; i < 40; i++) { + pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorDate `).length, - 1, - "should have a single date separator" // to check: may be client timezone dependent - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorLabelDate `).textContent, - "April 20, 2019", - "should display date day of messages" - ); - assert.strictEqual( - document.querySelectorAll(` + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 30, - "should have 30 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 30, + "should have 30 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 1, - "should have load more link" - ); + 1, + "should have load more link" + ); - await click(`.o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore`); - assert.strictEqual( - document.querySelectorAll(` + await click( + `.o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore` + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 40, - "should have 40 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 40, + "should have 40 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 0, - "should not longer have load more link (all messages loaded)" - ); -}); - -QUnit.test('auto-scroll to bottom of thread', async function (assert) { - // AKU TODO: thread specific test - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 1; i <= 25; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, + 0, + "should not longer have load more link (all messages loaded)" + ); }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel scrolled to its last message initially", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` + + QUnit.test("auto-scroll to bottom of thread", async function (assert) { + // AKU TODO: thread specific test + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 1; i <= 25; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 25, - "should have 25 messages" - ); - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - assert.ok( - isScrolledToBottom(messageList), - "should have scrolled to bottom of thread" - ); -}); - -QUnit.test('load more messages from channel (auto-load on scroll)', async function (assert) { - // AKU TODO: thread specific test - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 40; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, + 25, + "should have 25 messages" + ); + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + assert.ok(isScrolledToBottom(messageList), "should have scrolled to bottom of thread"); }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel scrolled to its last message initially", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message - `).length, - 30, - "should have 30 messages" - ); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_ThreadView_messageList').scrollTop = 0, - message: "should wait until channel loaded more messages after scrolling to top", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` + QUnit.test( + "load more messages from channel (auto-load on scroll)", + async function (assert) { + // AKU TODO: thread specific test + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 40; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 40, - "should have 40 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 30, + "should have 30 messages" + ); + + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => (document.querySelector(".o_ThreadView_messageList").scrollTop = 0), + message: + "should wait until channel loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message + `).length, + 40, + "should have 40 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_Dsiscuss_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 0, - "should not longer have load more link (all messages loaded)" - ); -}); + 0, + "should not longer have load more link (all messages loaded)" + ); + } + ); -QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) { - // this test requires several messages so that the last message is not - // visible. This is necessary in order to display 'new messages' and not - // remove from DOM right away from seeing last message. - // AKU TODO: thread specific test - assert.expect(6); + QUnit.test("new messages separator [REQUIRE FOCUS]", async function (assert) { + // this test requires several messages so that the last message is not + // visible. This is necessary in order to display 'new messages' and not + // remove from DOM right away from seeing last message. + // AKU TODO: thread specific test + assert.expect(6); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Foreigner partner" }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ uuid: 'randomuuid' }); - let lastMessageId; - for (let i = 1; i <= 25; i++) { - lastMessageId = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - } - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: lastMessageId }); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel scrolled to its last message initially", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Foreigner partner" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ uuid: "randomuuid" }); + let lastMessageId; + for (let i = 1; i <= 25; i++) { + lastMessageId = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: lastMessageId, + }); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.containsN( + document.body, + ".o_MessageListView_message", + 25, + "should have 25 messages" ); - }, - }); - assert.containsN( - document.body, - '.o_MessageListView_message', - 25, - "should have 25 messages" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - // scroll to top - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop = 0; - }, - message: "should wait until channel scrolled to top", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 0 + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" ); - }, - }); - // composer is focused by default, we remove that focus - document.querySelector('.o_ComposerTextInputView_textarea').blur(); - // simulate receiving a message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'randomuuid', - }, - })); - - assert.containsN( - document.body, - '.o_MessageListView_message', - 26, - "should have 26 messages" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should display 'new messages' separator" - ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight; - }, - message: "should wait until channel scrolled to bottom", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) + // scroll to top + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ).scrollTop = 0; + }, + message: "should wait until channel scrolled to top", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 0 + ); + }, + }); + // composer is focused by default, we remove that focus + document.querySelector(".o_ComposerTextInputView_textarea").blur(); + // simulate receiving a message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "randomuuid", + }, + }) ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should still display 'new messages' separator as composer is not focused" - ); - await afterNextRender(() => - document.querySelector('.o_ComposerTextInputView_textarea').focus() - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should no longer display 'new messages' separator (message seen)" - ); -}); + assert.containsN( + document.body, + ".o_MessageListView_message", + 26, + "should have 26 messages" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should display 'new messages' separator" + ); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight; + }, + message: "should wait until channel scrolled to bottom", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should still display 'new messages' separator as composer is not focused" + ); -QUnit.test('restore thread scroll position', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'Channel1' }, { name: 'Channel2' }]); - for (let i = 1; i <= 25; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, + await afterNextRender(() => + document.querySelector(".o_ComposerTextInputView_textarea").focus() + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should no longer display 'new messages' separator (message seen)" + ); }); - } - for (let i = 1; i <= 24; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId2, - }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel 1 scrolled to its last message", - predicate: ({ thread }) => { - return thread && thread.channel && thread.channel.id === mailChannelId1; - }, - }); - assert.strictEqual( - document.querySelectorAll(` + + QUnit.test("restore thread scroll position", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "Channel1" }, + { name: "Channel2" }, + ]); + for (let i = 1; i <= 25; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + for (let i = 1; i <= 24; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId2, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel 1 scrolled to its last message", + predicate: ({ thread }) => { + return thread && thread.channel && thread.channel.id === mailChannelId1; + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 25, - "should have 25 messages in channel 1" - ); - const initialMessageList = document.querySelector(` + 25, + "should have 25 messages in channel 1" + ); + const initialMessageList = document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList `); - assert.ok( - isScrolledToBottom(initialMessageList), - "should have scrolled to bottom of channel 1 initially" - ); + assert.ok( + isScrolledToBottom(initialMessageList), + "should have scrolled to bottom of channel 1 initially" + ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop = 0, - message: "should wait until channel 1 changed its scroll position to top", - predicate: ({ thread }) => { - return thread && thread.channel && thread.channel.id === mailChannelId1; - }, - }); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, - 0, - "should have scrolled to top of channel 1", - ); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + (document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ).scrollTop = 0), + message: "should wait until channel 1 changed its scroll position to top", + predicate: ({ thread }) => { + return thread && thread.channel && thread.channel.id === mailChannelId1; + }, + }); + assert.strictEqual( + document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, + 0, + "should have scrolled to top of channel 1" + ); - // Ensure scrollIntoView of channel 2 has enough time to complete before - // going back to channel 1. Await is needed to prevent the scrollIntoView - // initially planned for channel 2 to actually apply on channel 1. - // task-2333535 - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - // select channel 2 - document.querySelector(` + // Ensure scrollIntoView of channel 2 has enough time to complete before + // going back to channel 1. Await is needed to prevent the scrollIntoView + // initially planned for channel 2 to actually apply on channel 1. + // task-2333535 + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + // select channel 2 + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).click(); - }, - message: "should wait until channel 2 scrolled to its last message", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.channel && - thread.channel.id === mailChannelId2 && - isScrolledToBottom(messageList) - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .click(); + }, + message: "should wait until channel 2 scrolled to its last message", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.channel && + thread.channel.id === mailChannelId2 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 24, - "should have 24 messages in channel 2" - ); + 24, + "should have 24 messages in channel 2" + ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - // select channel 1 - document.querySelector(` + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + // select channel 1 + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] - `).click(); - }, - message: "should wait until channel 1 restored its scroll position", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.channel && - thread.channel.id === mailChannelId1 && - scrollTop === 0 + ` + ) + .click(); + }, + message: "should wait until channel 1 restored its scroll position", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.channel && + thread.channel.id === mailChannelId1 && + scrollTop === 0 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, + 0, + "should have recovered scroll position of channel 1 (scroll to top)" ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, - 0, - "should have recovered scroll position of channel 1 (scroll to top)" - ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - // select channel 2 - document.querySelector(` + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + // select channel 2 + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).click(); - }, - message: "should wait until channel 2 recovered its scroll position (to bottom)", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.channel && - thread.channel.id === mailChannelId2 && - isScrolledToBottom(messageList) + ` + ) + .click(); + }, + message: "should wait until channel 2 recovered its scroll position (to bottom)", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.channel && + thread.channel.id === mailChannelId2 && + isScrolledToBottom(messageList) + ); + }, + }); + const messageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(messageList), + "should have recovered scroll position of channel 2 (scroll to bottom)" ); - }, - }); - const messageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(messageList), - "should have recovered scroll position of channel 2 (scroll to bottom)" - ); -}); + }); -QUnit.test('redirect to author (open chat)', async function (assert) { - assert.expect(7); + QUnit.test("redirect to author (open chat)", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { name: "General" }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - } - ]); - const mailMessageId1 = pyEnv['mail.message'].create( - { - author_id: resPartnerId1, - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - } - ); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.ok( - document.querySelector(` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "General" }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }, + ]); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] - `).classList.contains('o-active'), - "channel 'General' should be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "channel 'General' should be active" + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).classList.contains('o-active'), - "Chat 'Demo' should not be active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 1, - "should have 1 message" - ); - const msg1 = document.querySelector(` + ` + ) + .classList.contains("o-active"), + "Chat 'Demo' should not be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 1, + "should have 1 message" + ); + const msg1 = document.querySelector(` .o_DiscussView_thread .o_MessageView[data-message-id="${mailMessageId1}"] `); - assert.strictEqual( - msg1.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, - 1, - "message1 should have author image" - ); - assert.ok( - msg1.querySelector(`:scope .o_MessageView_authorAvatar`).classList.contains('o_redirect'), - "message1 should have redirect to author" - ); + assert.strictEqual( + msg1.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, + 1, + "message1 should have author image" + ); + assert.ok( + msg1 + .querySelector(`:scope .o_MessageView_authorAvatar`) + .classList.contains("o_redirect"), + "message1 should have redirect to author" + ); - await afterNextRender(() => - msg1.querySelector(`:scope .o_MessageView_authorAvatar`).click() - ); - assert.notOk( - document.querySelector(` + await afterNextRender(() => + msg1.querySelector(`:scope .o_MessageView_authorAvatar`).click() + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] - `).classList.contains('o-active'), - "channel 'General' should become inactive after author redirection" - ); - assert.ok( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "channel 'General' should become inactive after author redirection" + ); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).classList.contains('o-active'), - "chat 'Demo' should become active after author redirection" - ); -}); + ` + ) + .classList.contains("o-active"), + "chat 'Demo' should become active after author redirection" + ); + }); -QUnit.test('sidebar quick search', async function (assert) { - // feature enables at 20 or more channels - assert.expect(6); + QUnit.test("sidebar quick search", async function (assert) { + // feature enables at 20 or more channels + assert.expect(6); - const pyEnv = await startServer(); - for (let id = 1; id <= 20; id++) { - pyEnv['mail.channel'].create({ name: `channel${id}` }); - } - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 20, - "should have 20 channel items" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_sidebar input.o_DiscussSidebarView_quickSearch`).length, - 1, - "should have quick search in sidebar" - ); + const pyEnv = await startServer(); + for (let id = 1; id <= 20; id++) { + pyEnv["mail.channel"].create({ name: `channel${id}` }); + } + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 20, + "should have 20 channel items" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussView_sidebar input.o_DiscussSidebarView_quickSearch` + ).length, + 1, + "should have quick search in sidebar" + ); - const quickSearch = document.querySelector(` + const quickSearch = document.querySelector(` .o_DiscussView_sidebar input.o_DiscussSidebarView_quickSearch `); - await afterNextRender(() => { - quickSearch.value = "1"; - const kevt1 = new window.KeyboardEvent('input'); - quickSearch.dispatchEvent(kevt1); - }); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 11, - "should have filtered to 11 channel items" - ); + await afterNextRender(() => { + quickSearch.value = "1"; + const kevt1 = new window.KeyboardEvent("input"); + quickSearch.dispatchEvent(kevt1); + }); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 11, + "should have filtered to 11 channel items" + ); - await afterNextRender(() => { - quickSearch.value = "12"; - const kevt2 = new window.KeyboardEvent('input'); - quickSearch.dispatchEvent(kevt2); - }); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 1, - "should have filtered to a single channel item" - ); - assert.strictEqual( - Number(document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).dataset.channelId), - pyEnv['mail.channel'].search([['name', '=', 'channel12']])[0], - "should have filtered to a single channel (channel12)" - ); + await afterNextRender(() => { + quickSearch.value = "12"; + const kevt2 = new window.KeyboardEvent("input"); + quickSearch.dispatchEvent(kevt2); + }); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have filtered to a single channel item" + ); + assert.strictEqual( + Number( + document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).dataset.channelId + ), + pyEnv["mail.channel"].search([["name", "=", "channel12"]])[0], + "should have filtered to a single channel (channel12)" + ); - await afterNextRender(() => { - quickSearch.value = "123"; - const kevt3 = new window.KeyboardEvent('input'); - quickSearch.dispatchEvent(kevt3); - }); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 0, - "should have filtered to no channel item" - ); -}); + await afterNextRender(() => { + quickSearch.value = "123"; + const kevt3 = new window.KeyboardEvent("input"); + quickSearch.dispatchEvent(kevt3); + }); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 0, + "should have filtered to no channel item" + ); + }); -QUnit.test('basic top bar rendering', async function (assert) { - assert.expect(8); + QUnit.test("basic top bar rendering", async function (assert) { + assert.expect(8); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelector(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelector(` .o_ThreadViewTopbar_threadName `).textContent, - "Inbox", - "display inbox in the top bar" - ); - const markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); - assert.isVisible( - markAllReadButton, - "should have visible button 'Mark all read' in the top bar of inbox" - ); - assert.ok( - markAllReadButton.disabled, - "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" - ); + "Inbox", + "display inbox in the top bar" + ); + const markAllReadButton = document.querySelector( + `.o_ThreadViewTopbar_markAllReadButton` + ); + assert.isVisible( + markAllReadButton, + "should have visible button 'Mark all read' in the top bar of inbox" + ); + assert.ok( + markAllReadButton.disabled, + "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `); - assert.strictEqual( - document.querySelector(` + assert.strictEqual( + document.querySelector(` .o_ThreadViewTopbar_threadName `).textContent, - "Starred", - "display starred in the breadcrumb" - ); - const unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); - assert.isVisible( - unstarAllButton, - "should have visible button 'Unstar all' in the top bar of starred" - ); - assert.ok( - unstarAllButton.disabled, - "should have disabled button 'Unstar all' in the top bar of starred (no messages)" - ); + "Starred", + "display starred in the breadcrumb" + ); + const unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); + assert.isVisible( + unstarAllButton, + "should have visible button 'Unstar all' in the top bar of starred" + ); + assert.ok( + unstarAllButton.disabled, + "should have disabled button 'Unstar all' in the top bar of starred (no messages)" + ); - await click(` + await click(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - document.querySelector(` + assert.strictEqual( + document.querySelector(` .o_ThreadViewTopbar_threadName `).textContent, - "General", - "display general in the breadcrumb" - ); - const inviteButton = document.querySelector(`.o_ThreadViewTopbar_inviteButton`); - assert.isVisible( - inviteButton, - "should have button 'Invite' in the top bar of channel" - ); -}); + "General", + "display general in the breadcrumb" + ); + const inviteButton = document.querySelector(`.o_ThreadViewTopbar_inviteButton`); + assert.isVisible(inviteButton, "should have button 'Invite' in the top bar of channel"); + }); -QUnit.test('inbox: mark all messages as read', async function (assert) { - assert.expect(8); + QUnit.test("inbox: mark all messages as read", async function (assert) { + assert.expect(8); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "not empty", - model: 'mail.channel', - needaction: true, - res_id: mailChannelId1, - }, - { - body: "not empty", - model: 'mail.channel', - needaction: true, - res_id: mailChannelId1, - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }, - { - mail_message_id: mailMessageId2, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - } - ]); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "not empty", + model: "mail.channel", + needaction: true, + res_id: mailChannelId1, + }, + { + body: "not empty", + model: "mail.channel", + needaction: true, + res_id: mailChannelId1, + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + { + mail_message_id: mailMessageId2, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + ]); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "2", - "inbox should have counter of 2" - ); - assert.strictEqual( - document.querySelector(` + "2", + "inbox should have counter of 2" + ); + assert.strictEqual( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] .o_DiscussSidebarCategoryItem_counter `).textContent, - "2", - "channel should have counter of 2" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 2, - "should have 2 messages in inbox" - ); - let markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); - assert.notOk( - markAllReadButton.disabled, - "should have enabled button 'Mark all read' in the top bar of inbox (has messages)" - ); + "2", + "channel should have counter of 2" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 2, + "should have 2 messages in inbox" + ); + let markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); + assert.notOk( + markAllReadButton.disabled, + "should have enabled button 'Mark all read' in the top bar of inbox (has messages)" + ); - await afterNextRender(() => markAllReadButton.click()); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + await afterNextRender(() => markAllReadButton.click()); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "inbox should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(` + 0, + "inbox should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] .o_DiscussSidebarCategoryItem_counter `).length, - 0, - "channel should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 0, - "should have no message in inbox" - ); - markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); - assert.ok( - markAllReadButton.disabled, - "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" - ); -}); + 0, + "channel should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 0, + "should have no message in inbox" + ); + markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); + assert.ok( + markAllReadButton.disabled, + "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" + ); + }); -QUnit.test('starred: unstar all', async function (assert) { - assert.expect(6); + QUnit.test("starred: unstar all", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - pyEnv['mail.message'].create([ - { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] }, - { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] } - ]); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_starred', - }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + const pyEnv = await startServer(); + pyEnv["mail.message"].create([ + { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] }, + { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] }, + ]); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_starred", + }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "2", - "starred should have counter of 2" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 2, - "should have 2 messages in starred" - ); - let unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); - assert.notOk( - unstarAllButton.disabled, - "should have enabled button 'Unstar all' in the top bar starred (has messages)" - ); + "2", + "starred should have counter of 2" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 2, + "should have 2 messages in starred" + ); + let unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); + assert.notOk( + unstarAllButton.disabled, + "should have enabled button 'Unstar all' in the top bar starred (has messages)" + ); - await afterNextRender(() => unstarAllButton.click()); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await afterNextRender(() => unstarAllButton.click()); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "starred should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 0, - "should have no message in starred" - ); - unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); - assert.ok( - unstarAllButton.disabled, - "should have disabled button 'Unstar all' in the top bar of starred (no messages)" - ); -}); + 0, + "starred should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 0, + "should have no message in starred" + ); + unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); + assert.ok( + unstarAllButton.disabled, + "should have disabled button 'Unstar all' in the top bar of starred (no messages)" + ); + }); -QUnit.test('toggle_star message', async function (assert) { - assert.expect(16); + QUnit.test("toggle_star message", async function (assert) { + assert.expect(16); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (args.method === 'toggle_message_starred') { - assert.step('rpc:toggle_message_starred'); - assert.strictEqual( - args.args[0][0], - mailMessageId1, - "should have message Id in args" - ); - } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (args.method === "toggle_message_starred") { + assert.step("rpc:toggle_message_starred"); + assert.strictEqual( + args.args[0][0], + mailMessageId1, + "should have message Id in args" + ); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "starred should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 1, - "should have 1 message in channel" - ); - let message = document.querySelector(`.o_DiscussView .o_MessageView`); - assert.notOk( - message.classList.contains('o-starred'), - "message should not be starred" - ); - await afterNextRender(() => message.click()); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "message should have star action" - ); + 0, + "starred should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 1, + "should have 1 message in channel" + ); + let message = document.querySelector(`.o_DiscussView .o_MessageView`); + assert.notOk(message.classList.contains("o-starred"), "message should not be starred"); + await afterNextRender(() => message.click()); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "message should have star action" + ); - await afterNextRender(() => message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click()); - assert.verifySteps(['rpc:toggle_message_starred']); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await afterNextRender(() => + message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click() + ); + assert.verifySteps(["rpc:toggle_message_starred"]); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "1", - "starred should display a counter of 1" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 1, - "should have kept 1 message in channel" - ); - message = document.querySelector(`.o_DiscussView .o_MessageView`); - assert.ok( - message.classList.contains('o-starred'), - "message should be starred" - ); + "1", + "starred should display a counter of 1" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 1, + "should have kept 1 message in channel" + ); + message = document.querySelector(`.o_DiscussView .o_MessageView`); + assert.ok(message.classList.contains("o-starred"), "message should be starred"); - await afterNextRender(() => message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click()); - assert.verifySteps(['rpc:toggle_message_starred']); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await afterNextRender(() => + message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click() + ); + assert.verifySteps(["rpc:toggle_message_starred"]); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "starred should no longer display a counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 1, - "should still have 1 message in channel" - ); - message = document.querySelector(`.o_DiscussView .o_MessageView`); - assert.notOk( - message.classList.contains('o-starred'), - "message should no longer be starred" - ); -}); + 0, + "starred should no longer display a counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 1, + "should still have 1 message in channel" + ); + message = document.querySelector(`.o_DiscussView .o_MessageView`); + assert.notOk( + message.classList.contains("o-starred"), + "message should no longer be starred" + ); + }); -QUnit.test('composer state: text save and restore', async function (assert) { - assert.expect(2); + QUnit.test("composer state: text save and restore", async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const [mailChannelId1] = pyEnv['mail.channel'].create([ - { name: "General" }, - { name: "Special" }, - ]); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - // Write text in composer for #general - await insertText('.o_ComposerTextInputView_textarea', "A message"); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); - await insertText('.o_ComposerTextInputView_textarea', "An other message"); - // Switch back to #general - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "A message", - "should restore the input text" - ); + const pyEnv = await startServer(); + const [mailChannelId1] = pyEnv["mail.channel"].create([ + { name: "General" }, + { name: "Special" }, + ]); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + // Write text in composer for #general + await insertText(".o_ComposerTextInputView_textarea", "A message"); + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); + await insertText(".o_ComposerTextInputView_textarea", "An other message"); + // Switch back to #general + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "A message", + "should restore the input text" + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "An other message", - "should restore the input text" - ); -}); + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "An other message", + "should restore the input text" + ); + }); -QUnit.test('composer state: attachments save and restore', async function (assert) { - assert.expect(6); + QUnit.test("composer state: attachments save and restore", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const [mailChannelId1] = pyEnv['mail.channel'].create([ - { name: "General" }, - { name: "Special" }, - ]); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - const channels = document.querySelectorAll(` + const pyEnv = await startServer(); + const [mailChannelId1] = pyEnv["mail.channel"].create([ + { name: "General" }, + { name: "Special" }, + ]); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + const channels = document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item `); - // Add attachment in a message for #general - await afterNextRender(async () => { - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', + // Add attachment in a message for #general + await afterNextRender(async () => { + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file, + ]); + }); + // Switch to #special + await afterNextRender(() => channels[1].click()); + // Add attachments in a message for #special + const files = [ + await createFile({ + content: "hello2, world", + contentType: "text/plain", + name: "text2.txt", + }), + await createFile({ + content: "hello3, world", + contentType: "text/plain", + name: "text3.txt", + }), + await createFile({ + content: "hello4, world", + contentType: "text/plain", + name: "text4.txt", + }), + ]; + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, files) + ); + // Switch back to #general + await afterNextRender(() => channels[0].click()); + // Check attachment is reloaded + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 1, + "should have 1 attachment in the composer" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerView .o_AttachmentCard`).dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 1 }).localId, + "should have correct 1st attachment in the composer" + ); + + // Switch back to #special + await afterNextRender(() => channels[1].click()); + // Check attachments are reloaded + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 3, + "should have 3 attachments in the composer" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[0].dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 2 }).localId, + "should have attachment with id 2 as 1st attachment" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[1].dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 3 }).localId, + "should have attachment with id 3 as 2nd attachment" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[2].dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 4 }).localId, + "should have attachment with id 4 as 3rd attachment" + ); }); - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - [file] - ); - }); - // Switch to #special - await afterNextRender(() => channels[1].click()); - // Add attachments in a message for #special - const files = [ - await createFile({ - content: 'hello2, world', - contentType: 'text/plain', - name: 'text2.txt', - }), - await createFile({ - content: 'hello3, world', - contentType: 'text/plain', - name: 'text3.txt', - }), - await createFile({ - content: 'hello4, world', - contentType: 'text/plain', - name: 'text4.txt', - }), - ]; - await afterNextRender(() => - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - files - ) - ); - // Switch back to #general - await afterNextRender(() => channels[0].click()); - // Check attachment is reloaded - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 1, - "should have 1 attachment in the composer" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerView .o_AttachmentCard`).dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 1 }).localId, - "should have correct 1st attachment in the composer" - ); - // Switch back to #special - await afterNextRender(() => channels[1].click()); - // Check attachments are reloaded - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 3, - "should have 3 attachments in the composer" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[0].dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 2 }).localId, - "should have attachment with id 2 as 1st attachment" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[1].dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 3 }).localId, - "should have attachment with id 3 as 2nd attachment" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[2].dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 4 }).localId, - "should have attachment with id 4 as 3rd attachment" - ); -}); + QUnit.test("post a simple message", async function (assert) { + assert.expect(16); -QUnit.test('post a simple message', async function (assert) { - assert.expect(16); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + assert.strictEqual( + args.thread_model, + "mail.channel", + "should post message to channel" + ); + assert.strictEqual( + args.thread_id, + mailChannelId1, + "should post message to channel 1" + ); + assert.strictEqual( + args.post_data.body, + "Test", + "should post with provided content in composer input" + ); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.post_data.subtype_xmlid, + "mail.mt_comment", + "should set subtype_xmlid as 'comment'" + ); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_MessageListView_empty`).length, + 1, + "should display thread with no message initially" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 0, + "should display no message initially" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "should have empty content initially" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); - assert.strictEqual( - args.thread_model, - 'mail.channel', - "should post message to channel" + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "Test", + "should have inserted text in editable" + ); + + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["message_post"]); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "should have no content in composer input after posting message" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 1, + "should display a message after posting message" + ); + const [postedMessageId] = pyEnv["mail.message"].search([], { order: "id DESC" }); + const message = document.querySelector(`.o_MessageView`); + assert.strictEqual( + parseInt(message.dataset.messageId), + postedMessageId, + "new message in thread should be linked to newly created message from message post" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_authorName`).textContent, + "Mitchell Admin", + "new message in thread should be from current partner name" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_content`).textContent, + "Test", + "new message in thread should have content typed from composer text input" + ); + }); + + QUnit.test( + 'post message on channel with "Enter" keyboard shortcut', + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in channel" ); - assert.strictEqual( - args.thread_id, - mailChannelId1, - "should post message to channel 1" - ); - assert.strictEqual( - args.post_data.body, - "Test", - "should post with provided content in composer input" - ); - assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" - ); - assert.strictEqual( - args.post_data.subtype_xmlid, - "mail.mt_comment", - "should set subtype_xmlid as 'comment'" + + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { key: "Enter" }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in channel after posting message from pressing 'Enter' in text input of composer" ); } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_MessageListView_empty`).length, - 1, - "should display thread with no message initially" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 0, - "should display no message initially" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "should have empty content initially" - ); + ); - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "Test", - "should have inserted text in editable" - ); + QUnit.test( + 'do not post message on channel with "SHIFT-Enter" keyboard shortcut', + async function (assert) { + // Note that test doesn't assert SHIFT-Enter makes a newline, because this + // default browser cannot be simulated with just dispatching + // programmatically crafted events... + assert.expect(2); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['message_post']); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "should have no content in composer input after posting message" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 1, - "should display a message after posting message" - ); - const [postedMessageId] = pyEnv['mail.message'].search([], { order: 'id DESC' }); - const message = document.querySelector(`.o_MessageView`); - assert.strictEqual( - parseInt(message.dataset.messageId), - postedMessageId, - "new message in thread should be linked to newly created message from message post" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_authorName`).textContent, - "Mitchell Admin", - "new message in thread should be from current partner name" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_content`).textContent, - "Test", - "new message in thread should have content typed from composer text input" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in channel" + ); -QUnit.test('post message on channel with "Enter" keyboard shortcut', async function (assert) { - assert.expect(2); + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + const kevt = new window.KeyboardEvent("keydown", { key: "Enter", shiftKey: true }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_MessageView", + "should still not have any message in channel after pressing 'Shift-Enter' in text input of composer" + ); + } + ); + QUnit.test("rendering of inbox message", async function (assert) { + // AKU TODO: kinda message specific test + assert.expect(8); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in channel" - ); - - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in channel after posting message from pressing 'Enter' in text input of composer" - ); -}); - -QUnit.test('do not post message on channel with "SHIFT-Enter" keyboard shortcut', async function (assert) { - // Note that test doesn't assert SHIFT-Enter makes a newline, because this - // default browser cannot be simulated with just dispatching - // programmatically crafted events... - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in channel" - ); - - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - const kevt = new window.KeyboardEvent('keydown', { key: "Enter", shiftKey: true }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_MessageView', - "should still not have any message in channel after pressing 'Shift-Enter' in text input of composer" - ); -}); -QUnit.test('rendering of inbox message', async function (assert) { - // AKU TODO: kinda message specific test - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Refactoring" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Refactoring" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(".o_MessageView").length, + 1, + "should display a message" ); - }, - }); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should display a message" - ); - const message = document.querySelector('.o_MessageView'); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_originThread`).length, - 1, - "should display origin thread of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_originThread`).textContent, - " on Refactoring", - "should display origin thread name" - ); - await afterNextRender(() => message.click()); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView`).length, - 4, - "should display 4 actions" - ); - assert.containsOnce( - document.body, - '.o_MessageActionView_actionReaction', - "should have action to add a reaction" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "should display star action" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionReplyTo`).length, - 1, - "should display reply action" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionMarkAsRead`).length, - 1, - "should display mark as read action" - ); -}); + const message = document.querySelector(".o_MessageView"); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_originThread`).length, + 1, + "should display origin thread of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_originThread`).textContent, + " on Refactoring", + "should display origin thread name" + ); + await afterNextRender(() => message.click()); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView`).length, + 4, + "should display 4 actions" + ); + assert.containsOnce( + document.body, + ".o_MessageActionView_actionReaction", + "should have action to add a reaction" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "should display star action" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionReplyTo`).length, + 1, + "should display reply action" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionMarkAsRead`).length, + 1, + "should display mark as read action" + ); + }); -QUnit.test('mark channel as seen on last message visible [REQUIRE FOCUS]', async function (assert) { - assert.expect(3); + QUnit.test( + "mark channel as seen on last message visible [REQUIRE FOCUS]", + async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { afterEvent, openDiscuss } = await start(); - await openDiscuss(); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "should have discuss sidebar item with the channel" - ); - assert.hasClass( - document.querySelector(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, openDiscuss } = await start(); + await openDiscuss(); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "should have discuss sidebar item with the channel" + ); + assert.hasClass( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `), - 'o-unread', - "sidebar item of channel 1 should be unread" - ); + "o-unread", + "sidebar item of channel 1 should be unread" + ); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => { - document.querySelector(` + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => { + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `).click(); - }, - message: "should wait until last seen by current partner message id changed", - predicate: ({ thread }) => { - return ( - thread.channel && - thread.channel.id === mailChannelId1 && - thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 - ); - }, - })); - assert.doesNotHaveClass( - document.querySelector(` + ` + ) + .click(); + }, + message: + "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return ( + thread.channel && + thread.channel.id === mailChannelId1 && + thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 + ); + }, + }) + ); + assert.doesNotHaveClass( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `), - 'o-unread', - "sidebar item of channel 1 should not longer be unread" - ); -}); + "o-unread", + "sidebar item of channel 1 should not longer be unread" + ); + } + ); -QUnit.test('receive new needaction messages', async function (assert) { - assert.expect(12); + QUnit.test("receive new needaction messages", async function (assert) { + assert.expect(12); - const { messaging, openDiscuss, pyEnv } = await start(); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const { messaging, openDiscuss, pyEnv } = await start(); + await openDiscuss(); + assert.ok( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `), - "should have inbox in sidebar" - ); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox should be current discuss thread" - ); - assert.notOk( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + "should have inbox in sidebar" + ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox should be current discuss thread" + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `), - "inbox item in sidebar should not have any counter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 0, - "should have no messages in inbox initially" - ); + "inbox item in sidebar should not have any counter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 0, + "should have no messages in inbox initially" + ); - // simulate receiving a new needaction message - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.message/inbox', { - 'body': "not empty", - 'id': 100, - 'needaction_partner_ids': [pyEnv.currentPartnerId], - 'model': 'res.partner', - 'res_id': 20, - }); - }); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + // simulate receiving a new needaction message + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.message/inbox", { + body: "not empty", + id: 100, + needaction_partner_ids: [pyEnv.currentPartnerId], + model: "res.partner", + res_id: 20, + }); + }); + assert.ok( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `), - "inbox item in sidebar should now have counter" - ); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + "inbox item in sidebar should now have counter" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - '1', - "inbox item in sidebar should have counter of '1'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 1, - "should have one message in inbox" - ); - assert.strictEqual( - parseInt(document.querySelector(`.o_DiscussView_thread .o_MessageView`).dataset.messageId), - 100, - "should display newly received needaction message" - ); + "1", + "inbox item in sidebar should have counter of '1'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 1, + "should have one message in inbox" + ); + assert.strictEqual( + parseInt( + document.querySelector(`.o_DiscussView_thread .o_MessageView`).dataset.messageId + ), + 100, + "should display newly received needaction message" + ); - // simulate receiving another new needaction message - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.message/inbox', { - 'body': "not empty", - 'id': 101, - 'needaction_partner_ids': [pyEnv.currentPartnerId], - 'model': 'res.partner', - 'res_id': 20, - }); - }); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + // simulate receiving another new needaction message + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.message/inbox", { + body: "not empty", + id: 101, + needaction_partner_ids: [pyEnv.currentPartnerId], + model: "res.partner", + res_id: 20, + }); + }); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - '2', - "inbox item in sidebar should have counter of '2'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 2, - "should have 2 messages in inbox" - ); - assert.ok( - document.querySelector(` + "2", + "inbox item in sidebar should have counter of '2'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 2, + "should have 2 messages in inbox" + ); + assert.ok( + document.querySelector(` .o_DiscussView_thread .o_MessageView[data-message-id="100"] `), - "should still display 1st needaction message" - ); - assert.ok( - document.querySelector(` + "should still display 1st needaction message" + ); + assert.ok( + document.querySelector(` .o_DiscussView_thread .o_MessageView[data-message-id="101"] `), - "should display 2nd needaction message" - ); -}); + "should display 2nd needaction message" + ); + }); -QUnit.test('reply to message from inbox (message linked to document)', async function (assert) { - assert.expect(19); + QUnit.test( + "reply to message from inbox (message linked to document)", + async function (assert) { + assert.expect(19); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Refactoring" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "Test
", - date: "2019-04-20 11:00:00", - message_type: 'comment', - needaction: true, - model: 'res.partner', - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); - const { click, insertText, openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Refactoring" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "Test
", + date: "2019-04-20 11:00:00", + message_type: "comment", + needaction: true, + model: "res.partner", + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + const { click, insertText, openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + assert.strictEqual( + args.thread_model, + "res.partner", + "should post message to record with model 'res.partner'" + ); + assert.strictEqual( + args.thread_id, + resPartnerId1, + "should post message to record with Id 20" + ); + assert.strictEqual( + args.post_data.body, + "Test", + "should post with provided content in composer input" + ); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + } + }, + services: { + notification: makeFakeNotificationService((notification) => { + assert.ok(true, "should display a notification after posting reply"); + assert.strictEqual( + notification, + 'Message posted on "Refactoring"', + "notification should tell that message has been posted to the record 'Refactoring'" + ); + }), + }, + }); + await openDiscuss(); assert.strictEqual( - args.thread_model, - 'res.partner', - "should post message to record with model 'res.partner'" + document.querySelectorAll(".o_MessageView").length, + 1, + "should display a single message" ); assert.strictEqual( - args.thread_id, - resPartnerId1, - "should post message to record with Id 20" + parseInt(document.querySelector(".o_MessageView").dataset.messageId), + mailMessageId1, + "should display message with ID 100" ); assert.strictEqual( - args.post_data.body, - "Test", - "should post with provided content in composer input" + document.querySelector(".o_MessageView_originThread").textContent, + " on Refactoring", + "should display message originates from record 'Refactoring'" + ); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.ok( + document.querySelector(".o_MessageView").classList.contains("o-selected"), + "message should be selected after clicking on reply icon" + ); + assert.ok( + document.querySelector(".o_ComposerView"), + "should have composer after clicking on reply to message" ); assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" + document.querySelector(`.o_ComposerView_threadName`).textContent, + " on: Refactoring", + "composer should display origin thread name of message" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer text input should be auto-focus" + ); + + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["message_post"]); + assert.notOk( + document.querySelector(".o_ComposerView"), + "should no longer have composer after posting reply to message" + ); + assert.strictEqual( + document.querySelectorAll(".o_MessageView").length, + 1, + "should still display a single message after posting reply" + ); + assert.strictEqual( + parseInt(document.querySelector(".o_MessageView").dataset.messageId), + mailMessageId1, + "should still display message with ID 100 after posting reply" + ); + assert.notOk( + document.querySelector(".o_MessageView").classList.contains("o-selected"), + "message should not longer be selected after posting reply" ); } - }, - services: { - notification: makeFakeNotificationService(notification => { - assert.ok( - true, - "should display a notification after posting reply" - ); - assert.strictEqual( - notification, - "Message posted on \"Refactoring\"", - "notification should tell that message has been posted to the record 'Refactoring'" - ); - }), - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should display a single message" - ); - assert.strictEqual( - parseInt(document.querySelector('.o_MessageView').dataset.messageId), - mailMessageId1, - "should display message with ID 100" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_originThread').textContent, - " on Refactoring", - "should display message originates from record 'Refactoring'" - ); + ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.ok( - document.querySelector('.o_MessageView').classList.contains('o-selected'), - "message should be selected after clicking on reply icon" - ); - assert.ok( - document.querySelector('.o_ComposerView'), - "should have composer after clicking on reply to message" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerView_threadName`).textContent, - " on: Refactoring", - "composer should display origin thread name of message" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer text input should be auto-focus" - ); + QUnit.test('messages marked as read move to "History" mailbox', async function (assert) { + assert.expect(10); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['message_post']); - assert.notOk( - document.querySelector('.o_ComposerView'), - "should no longer have composer after posting reply to message" - ); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should still display a single message after posting reply" - ); - assert.strictEqual( - parseInt(document.querySelector('.o_MessageView').dataset.messageId), - mailMessageId1, - "should still display message with ID 100 after posting reply" - ); - assert.notOk( - document.querySelector('.o_MessageView').classList.contains('o-selected'), - "message should not longer be selected after posting reply" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "not empty", + model: "mail.channel", // value to link message to channel + needaction: true, + res_id: mailChannelId1, // id of related channel + }, + { + body: "not empty", + model: "mail.channel", // value to link message to channel + needaction: true, + res_id: mailChannelId1, // id of related channel + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + { + mail_message_id: mailMessageId2, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + ]); + const { click, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_history", + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .classList.contains("o-active"), + "history mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 1, + "should have empty thread in history" + ); -QUnit.test('messages marked as read move to "History" mailbox', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "not empty", - model: 'mail.channel', // value to link message to channel - needaction: true, - res_id: mailChannelId1, // id of related channel - }, - { - body: "not empty", - model: 'mail.channel', // value to link message to channel - needaction: true, - res_id: mailChannelId1, // id of related channel - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }, - { - mail_message_id: mailMessageId2, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - } - ]); - const { click, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_history', - }, - }, - }); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).classList.contains('o-active'), - "history mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 1, - "should have empty thread in history" - ); - - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 0, - "inbox mailbox should not be empty" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`).length, - 2, - "inbox mailbox should have 2 messages" - ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 0, + "inbox mailbox should not be empty" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`) + .length, + 2, + "inbox mailbox should have 2 messages" + ); - await click('.o_ThreadViewTopbar_markAllReadButton'); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should still be active after mark as read" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 1, - "inbox mailbox should now be empty after mark as read" - ); + await click(".o_ThreadViewTopbar_markAllReadButton"); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should still be active after mark as read" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 1, + "inbox mailbox should now be empty after mark as read" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).classList.contains('o-active'), - "history mailbox should be active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 0, - "history mailbox should not be empty after mark as read" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`).length, - 2, - "history mailbox should have 2 messages" - ); -}); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .classList.contains("o-active"), + "history mailbox should be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 0, + "history mailbox should not be empty after mark as read" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`) + .length, + 2, + "history mailbox should have 2 messages" + ); + }); -QUnit.test('mark a single message as read should only move this message to "History" mailbox', async function (assert) { - assert.expect(9); + QUnit.test( + 'mark a single message as read should only move this message to "History" mailbox', + async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "not empty", - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - }, - { - body: "not empty", - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - { - mail_message_id: mailMessageId2, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - } - ]); - const { click, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_history', - }, - }, - }); - await openDiscuss(); - assert.hasClass( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + const pyEnv = await startServer(); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "not empty", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + }, + { + body: "not empty", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + const { click, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_history", + }, + }, + }); + await openDiscuss(); + assert.hasClass( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `), - 'o-active', - "history mailbox should initially be the active thread" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_empty', - "history mailbox should initially be empty" - ); + "o-active", + "history mailbox should initially be the active thread" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_empty", + "history mailbox should initially be empty" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.hasClass( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + assert.hasClass( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `), - 'o-active', - "inbox mailbox should be active thread after clicking on it" - ); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "inbox mailbox should have 2 messages" - ); + "o-active", + "inbox mailbox should be active thread after clicking on it" + ); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "inbox mailbox should have 2 messages" + ); - await click(` + await click(` .o_MessageView[data-message-id="${mailMessageId1}"] `); - await click(` + await click(` .o_MessageView[data-message-id="${mailMessageId1}"] .o_MessageActionView_actionMarkAsRead `); - assert.containsOnce( - document.body, - '.o_MessageView', - "inbox mailbox should have one less message after clicking mark as read" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-message-id="${mailMessageId2}"]`, - "message still in inbox should be the one not marked as read" - ); + assert.containsOnce( + document.body, + ".o_MessageView", + "inbox mailbox should have one less message after clicking mark as read" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-message-id="${mailMessageId2}"]`, + "message still in inbox should be the one not marked as read" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `); - assert.hasClass( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + assert.hasClass( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `), - 'o-active', - "history mailbox should be active after clicking on it" - ); - assert.containsOnce( - document.body, - '.o_MessageView', - "history mailbox should have only 1 message after mark as read" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-message-id="${mailMessageId1}"]`, - "message moved in history should be the one marked as read" - ); -}); + "o-active", + "history mailbox should be active after clicking on it" + ); + assert.containsOnce( + document.body, + ".o_MessageView", + "history mailbox should have only 1 message after mark as read" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-message-id="${mailMessageId1}"]`, + "message moved in history should be the one marked as read" + ); + } + ); -QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) { - assert.expect(2); + QUnit.test( + 'all messages in "Inbox" in "History" after marked all as read', + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - for (let i = 0; i < 40; i++) { - const currentMailMessageId = pyEnv['mail.message'].create({ - body: "not empty", - needaction: true, - }); - pyEnv['mail.notification'].create({ - mail_message_id: currentMailMessageId, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); + const pyEnv = await startServer(); + for (let i = 0; i < 40; i++) { + const currentMailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + needaction: true, + }); + pyEnv["mail.notification"].create({ + mail_message_id: currentMailMessageId, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + } + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until inbox scrolled to its last message initially", + predicate: ({ orderedMessages, scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread === messaging.inbox.thread && + orderedMessages.length === 30 && + isScrolledToBottom(messageList) + ); + }, + }); - } - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until inbox scrolled to its last message initially", - predicate: ({ orderedMessages, scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread === messaging.inbox.thread && - orderedMessages.length === 30 && - isScrolledToBottom(messageList) - ); - }, - }); + await click(".o_ThreadViewTopbar_markAllReadButton"); + assert.containsNone( + document.body, + ".o_MessageView", + "there should no message in Inbox anymore" + ); - await click('.o_ThreadViewTopbar_markAllReadButton'); - assert.containsNone( - document.body, - '.o_MessageView', - "there should no message in Inbox anymore" - ); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .click(); + }, + message: + "should wait until history scrolled to its last message after opening it from the discuss sidebar", + predicate: ({ orderedMessages, scrollTop, thread }) => { + const messageList = document.querySelector(".o_MessageListView"); + return ( + thread && + thread.mailbox && + thread.mailbox === messaging.history && + orderedMessages.length === 30 && + isScrolledToBottom(messageList) + ); + }, + }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).click(); - }, - message: "should wait until history scrolled to its last message after opening it from the discuss sidebar", - predicate: ({ orderedMessages, scrollTop, thread }) => { - const messageList = document.querySelector('.o_MessageListView'); - return ( - thread && - thread.mailbox && - thread.mailbox === messaging.history && - orderedMessages.length === 30 && - isScrolledToBottom(messageList) - ); - }, - }); + // simulate a scroll to top to load more messages + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => (document.querySelector(".o_MessageListView").scrollTop = 0), + message: + "should wait until mailbox history loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.mailbox && + threadViewer.thread.mailbox === messaging.history + ); + }, + }); + assert.containsN( + document.body, + ".o_MessageView", + 40, + "there should be 40 messages in History" + ); + } + ); - // simulate a scroll to top to load more messages - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_MessageListView').scrollTop = 0, - message: "should wait until mailbox history loaded more messages after scrolling to top", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.mailbox && - threadViewer.thread.mailbox === messaging.history - ); - }, - }); - assert.containsN( - document.body, - '.o_MessageView', - 40, - "there should be 40 messages in History" - ); -}); + QUnit.test( + "receive new chat message: out of odoo focus (notification, channel)", + async function (assert) { + assert.expect(4); -QUnit.test('receive new chat message: out of odoo focus (notification, channel)', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ channel_type: "chat" }); + const { env, openDiscuss } = await start({ + services: { + presence: makeFakePresenceService({ isOdooFocused: () => false }), + }, + }); + await openDiscuss(); + env.bus.addEventListener("set_title_part", ({ detail: payload }) => { + assert.step("set_title_part"); + assert.strictEqual(payload.part, "_chat"); + assert.strictEqual(payload.title, "1 Message"); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: 'chat' }); - const { env, openDiscuss } = await start({ - services: { - presence: makeFakePresenceService({ isOdooFocused: () => false }), - }, - }); - await openDiscuss(); - env.bus.addEventListener('set_title_part', ({ detail: payload }) => { - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - assert.strictEqual(payload.title, "1 Message"); - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + // simulate receiving a new message with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + id: 126, + model: "mail.channel", + res_id: mailChannelId1, + }, + }); + }); + assert.verifySteps(["set_title_part"]); + } + ); - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receiving a new message with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', { - 'id': mailChannelId1, - 'message': { - id: 126, - model: 'mail.channel', - res_id: mailChannelId1, - }, - }); - }); - assert.verifySteps(['set_title_part']); -}); + QUnit.test( + "receive new chat message: out of odoo focus (notification, chat)", + async function (assert) { + assert.expect(4); -QUnit.test('receive new chat message: out of odoo focus (notification, chat)', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ channel_type: "chat" }); + const { env, openDiscuss } = await start({ + services: { + presence: makeFakePresenceService({ isOdooFocused: () => false }), + }, + }); + await openDiscuss(); + env.bus.addEventListener("set_title_part", ({ detail: payload }) => { + assert.step("set_title_part"); + assert.strictEqual(payload.part, "_chat"); + assert.strictEqual(payload.title, "1 Message"); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: "chat" }); - const { env, openDiscuss } = await start({ - services: { - presence: makeFakePresenceService({ isOdooFocused: () => false }), - }, - }); - await openDiscuss(); - env.bus.addEventListener('set_title_part', ({ detail: payload }) => { - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - assert.strictEqual(payload.title, "1 Message"); - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + // simulate receiving a new message with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + id: 126, + model: "mail.channel", + res_id: mailChannelId1, + }, + }); + }); + assert.verifySteps(["set_title_part"]); + } + ); - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receiving a new message with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', { - 'id': mailChannelId1, - 'message': { - id: 126, - model: 'mail.channel', - res_id: mailChannelId1, - }, - }); - }); - assert.verifySteps(['set_title_part']); -}); + QUnit.test( + "receive new chat messages: out of odoo focus (tab title)", + async function (assert) { + assert.expect(12); -QUnit.test('receive new chat messages: out of odoo focus (tab title)', async function (assert) { - assert.expect(12); + let step = 0; + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { channel_type: "chat" }, + { channel_type: "chat" }, + ]); + const { env, openDiscuss } = await start({ + services: { + presence: makeFakePresenceService({ isOdooFocused: () => false }), + }, + }); + await openDiscuss(); + env.bus.addEventListener("set_title_part", ({ detail: payload }) => { + step++; + assert.step("set_title_part"); + assert.strictEqual(payload.part, "_chat"); + if (step === 1) { + assert.strictEqual(payload.title, "1 Message"); + } + if (step === 2) { + assert.strictEqual(payload.title, "2 Messages"); + } + if (step === 3) { + assert.strictEqual(payload.title, "3 Messages"); + } + }); - let step = 0; - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { channel_type: 'chat' }, - { channel_type: 'chat' }, - ]); - const { env, openDiscuss } = await start({ - services: { - presence: makeFakePresenceService({ isOdooFocused: () => false }), - }, - }); - await openDiscuss(); - env.bus.addEventListener('set_title_part', ({ detail: payload }) => { - step++; - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - if (step === 1) { - assert.strictEqual(payload.title, "1 Message"); - } - if (step === 2) { - assert.strictEqual(payload.title, "2 Messages"); - } - if (step === 3) { - assert.strictEqual(payload.title, "3 Messages"); - } - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + // simulate receiving a new message in chat 1 with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + id: 126, + model: "mail.channel", + res_id: mailChannelId1, + }, + }); + }); + assert.verifySteps(["set_title_part"]); - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receiving a new message in chat 1 with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', { - 'id': mailChannelId1, - 'message': { - id: 126, - model: 'mail.channel', - res_id: mailChannelId1, - }, - }); - }); - assert.verifySteps(['set_title_part']); + const mailChannel2 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId2], + ])[0]; + // simulate receiving a new message in chat 2 with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel2, "mail.channel/new_message", { + id: mailChannelId2, + message: { + id: 127, + model: "mail.channel", + res_id: mailChannelId2, + }, + }); + }); + assert.verifySteps(["set_title_part"]); - const mailChannel2 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId2]])[0]; - // simulate receiving a new message in chat 2 with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel2, 'mail.channel/new_message', { - 'id': mailChannelId2, - 'message': { - id: 127, - model: 'mail.channel', - res_id: mailChannelId2, - }, - }); - }); - assert.verifySteps(['set_title_part']); + // simulate receiving another new message in chat 2 with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel2, "mail.channel/new_message", { + id: mailChannelId2, + message: { + id: 128, + model: "mail.channel", + res_id: mailChannelId2, + }, + }); + }); + assert.verifySteps(["set_title_part"]); + } + ); - // simulate receiving another new message in chat 2 with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel2, 'mail.channel/new_message', { - 'id': mailChannelId2, - 'message': { - id: 128, - model: 'mail.channel', - res_id: mailChannelId2, - }, - }); - }); - assert.verifySteps(['set_title_part']); -}); + QUnit.test("auto-focus composer on opening thread", async function (assert) { + assert.expect(14); -QUnit.test('auto-focus composer on opening thread', async function (assert) { - assert.expect(14); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - pyEnv['mail.channel'].create([ - { name: "General" }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - } - ]); - const { click, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + pyEnv["mail.channel"].create([ + { name: "General" }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }, + ]); + const { click, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarMailboxView[data-mailbox-name="Inbox"] `).length, - 1, - "should have mailbox 'Inbox' in the sidebar" - ); - assert.ok( - document.querySelector(` + 1, + "should have mailbox 'Inbox' in the sidebar" + ); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarMailboxView[data-mailbox-name="Inbox"] - `).classList.contains('o-active'), - "mailbox 'Inbox' should be active initially" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "mailbox 'Inbox' should be active initially" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-name="General"] `).length, - 1, - "should have channel 'General' in the sidebar" - ); - assert.notOk( - document.querySelector(` + 1, + "should have channel 'General' in the sidebar" + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-name="General"] - `).classList.contains('o-active'), - "channel 'General' should not be active initially" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "channel 'General' should not be active initially" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-name="Demo User"] `).length, - 1, - "should have chat 'Demo User' in the sidebar" - ); - assert.notOk( - document.querySelector(` + 1, + "should have chat 'Demo User' in the sidebar" + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-name="Demo User"] - `).classList.contains('o-active'), - "chat 'Demo User' should not be active initially" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView`).length, - 0, - "there should be no composer when active thread of discuss is mailbox 'Inbox'" - ); + ` + ) + .classList.contains("o-active"), + "chat 'Demo User' should not be active initially" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView`).length, + 0, + "there should be no composer when active thread of discuss is mailbox 'Inbox'" + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); - assert.ok( - document.querySelector(` + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-name="General"] - `).classList.contains('o-active'), - "channel 'General' should become active after selecting it from the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView`).length, - 1, - "there should be a composer when active thread of discuss is channel 'General'" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer of channel 'General' should be automatically focused on opening" - ); + ` + ) + .classList.contains("o-active"), + "channel 'General' should become active after selecting it from the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView`).length, + 1, + "there should be a composer when active thread of discuss is channel 'General'" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer of channel 'General' should be automatically focused on opening" + ); - document.querySelector(`.o_ComposerTextInputView_textarea`).blur(); - assert.notOk( - document.activeElement === document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer of channel 'General' should no longer focused on click away" - ); + document.querySelector(`.o_ComposerTextInputView_textarea`).blur(); + assert.notOk( + document.activeElement === + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer of channel 'General' should no longer focused on click away" + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Demo User"]`); - assert.ok( - document.querySelector(` + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Demo User"]`); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarCategory_item[data-channel-name="Demo User"] - `).classList.contains('o-active'), - "chat 'Demo User' should become active after selecting it from the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView`).length, - 1, - "there should be a composer when active thread of discuss is chat 'Demo User'" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer of chat 'Demo User' should be automatically focused on opening" - ); -}); - -QUnit.test('mark channel as seen if last message is visible when switching channels when the previous channel had a more recent last message than the current channel [REQUIRE FOCUS]', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: 'Bla', - }, - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: 'Blu', - }, - ]); - const [mailMessageId1] = pyEnv['mail.message'].create([{ - body: 'oldest message', - model: "mail.channel", - res_id: mailChannelId1, - }, { - body: 'newest message', - model: "mail.channel", - res_id: mailChannelId2, - }]); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId2}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until channel 2 loaded its messages initially", - predicate: ({ hint, threadViewer }) => { - return ( - threadViewer.thread.channel && - threadViewer.thread.channel.id === mailChannelId2 && - hint.type === 'messages-loaded' + ` + ) + .classList.contains("o-active"), + "chat 'Demo User' should become active after selecting it from the sidebar" ); - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => { - document.querySelector(` + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView`).length, + 1, + "there should be a composer when active thread of discuss is chat 'Demo User'" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer of chat 'Demo User' should be automatically focused on opening" + ); + }); + + QUnit.test( + "mark channel as seen if last message is visible when switching channels when the previous channel had a more recent last message than the current channel [REQUIRE FOCUS]", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Bla", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Blu", + }, + ]); + const [mailMessageId1] = pyEnv["mail.message"].create([ + { + body: "oldest message", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + body: "newest message", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId2}`, + }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until channel 2 loaded its messages initially", + predicate: ({ hint, threadViewer }) => { + return ( + threadViewer.thread.channel && + threadViewer.thread.channel.id === mailChannelId2 && + hint.type === "messages-loaded" + ); + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => { + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `).click(); - }, - message: "should wait until last seen by current partner message id changed", - predicate: ({ thread }) => { - return ( - thread.channel && - thread.channel.id === mailChannelId1 && - thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 - ); - }, - })); - assert.doesNotHaveClass( - document.querySelector(` + ` + ) + .click(); + }, + message: + "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return ( + thread.channel && + thread.channel.id === mailChannelId1 && + thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 + ); + }, + }) + ); + assert.doesNotHaveClass( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `), - 'o-unread', - "sidebar item of channel 1 should no longer be unread" - ); -}); - -QUnit.test('warning on send with shortcut when attempting to post message with still-uploading attachments', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - // simulates attachment is never finished uploading - await new Promise(() => {}); - } - }, - services: { - notification: makeFakeNotificationService((message, options) => { - assert.strictEqual( - message, - "Please wait while the file is uploading.", - "notification content should be about the uploading file" + "o-unread", + "sidebar item of channel 1 should no longer be unread" ); - assert.strictEqual( - options.type, - 'warning', - "notification should be a warning" - ); - assert.step('notification'); - }), - }, - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - [file] - ) - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should have only one attachment" - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard.o-isUploading', - "attachment displayed is being uploaded" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonSend', - "composer send button should be displayed" - ); - - // Try to send message - document - .querySelector(`.o_ComposerTextInputView_textarea`) - .dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter' })); - assert.verifySteps( - ['notification'], - "should have triggered a notification for inability to post message at the moment (some attachments are still being uploaded)" - ); -}); - -QUnit.test('send message only once when enter is pressed twice quickly', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); } - }, - }); - await openDiscuss(); - // Type message - await insertText('.o_ComposerTextInputView_textarea', "test message"); - await afterNextRender(() => { - const enterEvent = new window.KeyboardEvent('keydown', { key: 'Enter' }); - document.querySelector(`.o_ComposerTextInputView_textarea`) - .dispatchEvent(enterEvent); - document.querySelector(`.o_ComposerTextInputView_textarea`) - .dispatchEvent(enterEvent); - }); - assert.verifySteps( - ['message_post'], - "The message has been posted only once" - ); -}); + ); -QUnit.test('message being a replied to another message should show message being replied in the message view', async function (assert) { - assert.expect(1); + QUnit.test( + "warning on send with shortcut when attempting to post message with still-uploading attachments", + async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "1st message", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const mailMessageId2 = pyEnv['mail.message'].create({ - body: "2nd message", - model: 'mail.channel', - parent_id: mailMessageId1, - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.querySelector(`.o_MessageView[data-message-id="${mailMessageId2}"]`), - '.o_MessageInReplyToView', - "message being a replied to another message should show message being replied in the message view", - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + // simulates attachment is never finished uploading + await new Promise(() => {}); + } + }, + services: { + notification: makeFakeNotificationService((message, options) => { + assert.strictEqual( + message, + "Please wait while the file is uploading.", + "notification content should be about the uploading file" + ); + assert.strictEqual( + options.type, + "warning", + "notification should be a warning" + ); + assert.step("notification"); + }), + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file, + ]) + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard", + "should have only one attachment" + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard.o-isUploading", + "attachment displayed is being uploaded" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonSend", + "composer send button should be displayed" + ); -}); + // Try to send message + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter" })); + assert.verifySteps( + ["notification"], + "should have triggered a notification for inability to post message at the moment (some attachments are still being uploaded)" + ); + } + ); + + QUnit.test( + "send message only once when enter is pressed twice quickly", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + } + }, + }); + await openDiscuss(); + // Type message + await insertText(".o_ComposerTextInputView_textarea", "test message"); + await afterNextRender(() => { + const enterEvent = new window.KeyboardEvent("keydown", { key: "Enter" }); + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .dispatchEvent(enterEvent); + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .dispatchEvent(enterEvent); + }); + assert.verifySteps(["message_post"], "The message has been posted only once"); + } + ); + + QUnit.test( + "message being a replied to another message should show message being replied in the message view", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "1st message", + model: "mail.channel", + res_id: mailChannelId1, + }); + const mailMessageId2 = pyEnv["mail.message"].create({ + body: "2nd message", + model: "mail.channel", + parent_id: mailMessageId1, + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.querySelector(`.o_MessageView[data-message-id="${mailMessageId2}"]`), + ".o_MessageInReplyToView", + "message being a replied to another message should show message being replied in the message view" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js index ed9f43f7a63..815b41ec8bd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js @@ -13,11 +13,11 @@ QUnit.module("Field text emojis", (hooks) => { models: { partner: { fields: { - foo: { type: "char" } + foo: { type: "char" }, }, - records: [{ id: 1 }] - } - } + records: [{ id: 1 }], + }, + }, }; setupViewRegistries(); @@ -29,7 +29,7 @@ QUnit.module("Field text emojis", (hooks) => { resId: 1, resModel: "partner", arch: ``, - serverData + serverData, }); assert.containsOnce(target, ".o_field_text_emojis"); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js index af096065f28..6ccec694a59 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js @@ -1,183 +1,177 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follow_button_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follow_button_tests.js"); -QUnit.test('base rendering not editable', async function (assert) { - assert.expect(2); + QUnit.test("base rendering not editable", async function (assert) { + assert.expect(2); - const { openView, pyEnv } = await start(); - await openView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - views: [[false, 'form']], + const { openView, pyEnv } = await start(); + await openView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_follow", + "should have 'Follow' button" + ); + }); + + QUnit.test("hover following button", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const followerId = pyEnv["mail.followers"].create({ + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: threadId, + res_model: "res.partner", + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_unfollow", + "should have 'Unfollow' button" + ); + assert.strictEqual( + document.querySelector(".o_FollowButtonView_text").textContent.trim(), + "Following", + "'unfollow' button should display 'Following' as text when not hovered" + ); + assert.containsNone( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-times", + "'unfollow' button should not contain a cross icon when not hovered" + ); + assert.containsOnce( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-check", + "'unfollow' button should contain a check icon when not hovered" + ); + + await afterNextRender(() => { + document + .querySelector(".o_FollowButtonView_unfollow") + .dispatchEvent(new window.MouseEvent("mouseenter")); + }); + assert.strictEqual( + document.querySelector(".o_FollowButtonView_text").textContent.trim(), + "Unfollow", + "'unfollow' button should display 'Unfollow' as text when hovered" + ); + assert.containsOnce( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-times", + "'unfollow' button should contain a cross icon when hovered" + ); + assert.containsNone( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-check", + "'unfollow' button should not contain a check icon when hovered" + ); + }); + + QUnit.test('click on "follow" button', async function (assert) { + assert.expect(4); + + const { click, openView, pyEnv } = await start(); + await openView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_follow", + "should have button follow" + ); + + await click(".o_FollowButtonView_follow"); + assert.containsNone( + document.body, + ".o_FollowButtonView_follow", + "should not have follow button after clicked on follow" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_unfollow", + "should have unfollow button after clicked on follow" + ); + }); + + QUnit.test('click on "unfollow" button', async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + pyEnv["mail.followers"].create({ + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: threadId, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsNone( + document.body, + ".o_FollowButtonView_follow", + "should not have button follow" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_unfollow", + "should have button unfollow" + ); + + await click(".o_FollowButtonView_unfollow"); + assert.containsOnce( + document.body, + ".o_FollowButtonView_follow", + "should have follow button after clicked on unfollow" + ); + assert.containsNone( + document.body, + ".o_FollowButtonView_unfollow", + "should not have unfollow button after clicked on unfollow" + ); + }); }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_follow', - "should have 'Follow' button" - ); -}); - -QUnit.test('hover following button', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const followerId = pyEnv['mail.followers'].create({ - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: threadId, - res_model: 'res.partner', - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_unfollow', - "should have 'Unfollow' button" - ); - assert.strictEqual( - document.querySelector('.o_FollowButtonView_text').textContent.trim(), - 'Following', - "'unfollow' button should display 'Following' as text when not hovered" - ); - assert.containsNone( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-times', - "'unfollow' button should not contain a cross icon when not hovered" - ); - assert.containsOnce( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-check', - "'unfollow' button should contain a check icon when not hovered" - ); - - await afterNextRender(() => { - document - .querySelector('.o_FollowButtonView_unfollow') - .dispatchEvent(new window.MouseEvent('mouseenter')); - } - ); - assert.strictEqual( - document.querySelector('.o_FollowButtonView_text').textContent.trim(), - 'Unfollow', - "'unfollow' button should display 'Unfollow' as text when hovered" - ); - assert.containsOnce( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-times', - "'unfollow' button should contain a cross icon when hovered" - ); - assert.containsNone( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-check', - "'unfollow' button should not contain a check icon when hovered" - ); -}); - -QUnit.test('click on "follow" button', async function (assert) { - assert.expect(4); - - const { click, openView, pyEnv } = await start(); - await openView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_follow', - "should have button follow" - ); - - await click('.o_FollowButtonView_follow'); - assert.containsNone( - document.body, - '.o_FollowButtonView_follow', - "should not have follow button after clicked on follow" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_unfollow', - "should have unfollow button after clicked on follow" - ); -}); - -QUnit.test('click on "unfollow" button', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - pyEnv['mail.followers'].create({ - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: threadId, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsNone( - document.body, - '.o_FollowButtonView_follow', - "should not have button follow" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_unfollow', - "should have button unfollow" - ); - - await click('.o_FollowButtonView_unfollow'); - assert.containsOnce( - document.body, - '.o_FollowButtonView_follow', - "should have follow button after clicked on unfollow" - ); - assert.containsNone( - document.body, - '.o_FollowButtonView_unfollow', - "should not have unfollow button after clicked on unfollow" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js index f9a954cd8fc..1f6d23026bd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js @@ -1,457 +1,462 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -import { nextTick } from 'web.test_utils'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { nextTick } from "web.test_utils"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follower_list_menu_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follower_list_menu_tests.js"); -QUnit.test('base rendering not editable', async function (assert) { - assert.expect(5); + QUnit.test("base rendering not editable", async function (assert) { + assert.expect(5); - const { openView } = await start(); - await openView( - { - res_model: 'res.partner', - views: [[false, 'form']], - }, - { mode: 'edit' }, - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - assert.ok( - document.querySelector('.o_FollowerListMenuView_buttonFollowers').disabled, - "followers button should be disabled" - ); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should not be opened" - ); + const { openView } = await start(); + await openView( + { + res_model: "res.partner", + views: [[false, "form"]], + }, + { mode: "edit" } + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" + ); + assert.ok( + document.querySelector(".o_FollowerListMenuView_buttonFollowers").disabled, + "followers button should be disabled" + ); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should not be opened" + ); - document.querySelector('.o_FollowerListMenuView_buttonFollowers').click(); - await nextTick(); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should still be closed as button is disabled" - ); -}); + document.querySelector(".o_FollowerListMenuView_buttonFollowers").click(); + await nextTick(); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should still be closed as button is disabled" + ); + }); -QUnit.test('base rendering editable', async function (assert) { - assert.expect(5); + QUnit.test("base rendering editable", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - assert.notOk( - document.querySelector('.o_FollowerListMenuView_buttonFollowers').disabled, - "followers button should not be disabled" - ); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should not be opened" - ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" + ); + assert.notOk( + document.querySelector(".o_FollowerListMenuView_buttonFollowers").disabled, + "followers button should not be disabled" + ); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should not be opened" + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be opened" - ); -}); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be opened" + ); + }); -QUnit.test('click on "add followers" button', async function (assert) { - assert.expect(15); + QUnit.test('click on "add followers" button', async function (assert) { + assert.expect(15); - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { name: 'resPartner1' }, - { name: 'resPartner2' }, - { name: 'resPartner3' }, - ]); - pyEnv['mail.followers'].create({ - partner_id: resPartnerId2, - email: "bla@bla.bla", - is_active: true, - name: "François Perusse", - res_id: resPartnerId1, - res_model: 'res.partner', - }); + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + { name: "resPartner3" }, + ]); + pyEnv["mail.followers"].create({ + partner_id: resPartnerId2, + email: "bla@bla.bla", + is_active: true, + name: "François Perusse", + res_id: resPartnerId1, + res_model: "res.partner", + }); - const { click, env, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action, options) { - assert.step('action:open_view'); - assert.strictEqual( - action.context.default_res_model, - 'res.partner', - "'The 'add followers' action should contain thread model in context'" + const { click, env, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { + assert.step("action:open_view"); + assert.strictEqual( + action.context.default_res_model, + "res.partner", + "'The 'add followers' action should contain thread model in context'" + ); + assert.strictEqual( + action.context.default_res_id, + resPartnerId1, + "The 'add followers' action should contain thread id in context" + ); + assert.strictEqual( + action.res_model, + "mail.wizard.invite", + "The 'add followers' action should be a wizard invite of mail module" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + "The 'add followers' action should be of type 'ir.actions.act_window'" + ); + pyEnv["mail.followers"].create({ + partner_id: resPartnerId3, + email: "bla@bla.bla", + is_active: true, + name: "Wololo", + res_id: resPartnerId1, + res_model: "res.partner", + }); + options.onClose(); + }, + }); + + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" ); assert.strictEqual( - action.context.default_res_id, - resPartnerId1, - "The 'add followers' action should contain thread id in context" + document.querySelector(".o_FollowerListMenuView_buttonFollowersCount").textContent, + "1", + "Followers counter should be equal to 1" + ); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be opened" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_addFollowersButton", + "followers dropdown should contain a 'Add followers' button" + ); + + await click(".o_FollowerListMenuView_addFollowersButton"); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be closed after click on 'Add followers'" + ); + assert.verifySteps(["action:open_view"]); + assert.strictEqual( + document.querySelector(".o_FollowerListMenuView_buttonFollowersCount").textContent, + "2", + "Followers counter should now be equal to 2" + ); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsN( + document.body, + ".o_FollowerMenu_follower", + 2, + "Follower list should be refreshed and contain 2 followers" ); assert.strictEqual( - action.res_model, - 'mail.wizard.invite', - "The 'add followers' action should be a wizard invite of mail module" + document.querySelector(".o_FollowerView_name").textContent, + "François Perusse", + "Follower added in follower list should be the one added" ); - assert.strictEqual( - action.type, - "ir.actions.act_window", - "The 'add followers' action should be of type 'ir.actions.act_window'" - ); - pyEnv['mail.followers'].create({ - partner_id: resPartnerId3, + }); + + QUnit.test("click on remove follower", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + ]); + pyEnv["mail.followers"].create({ + partner_id: resPartnerId2, email: "bla@bla.bla", is_active: true, name: "Wololo", res_id: resPartnerId1, - res_model: 'res.partner', + res_model: "res.partner", + }); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = true; + return res; + } + if (route.includes("message_unsubscribe")) { + assert.step("message_unsubscribe"); + assert.deepEqual( + args.args, + [[resPartnerId1], [resPartnerId2]], + "message_unsubscribe should be called with right argument" + ); + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], }); - options.onClose(); - }, - }); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - assert.strictEqual( - document.querySelector('.o_FollowerListMenuView_buttonFollowersCount').textContent, - "1", - "Followers counter should be equal to 1" - ); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_removeButton", + "should display a remove button" + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be opened" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_addFollowersButton', - "followers dropdown should contain a 'Add followers' button" - ); + await click(".o_FollowerView_removeButton"); + assert.verifySteps( + ["message_unsubscribe"], + "clicking on remove button should call 'message_unsubscribe' route" + ); + assert.containsNone( + document.body, + ".o_FollowerView", + "should no longer have follower component" + ); + }); - await click('.o_FollowerListMenuView_addFollowersButton'); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be closed after click on 'Add followers'" - ); - assert.verifySteps([ - 'action:open_view', - ]); - assert.strictEqual( - document.querySelector('.o_FollowerListMenuView_buttonFollowersCount').textContent, - "2", - "Followers counter should now be equal to 2" - ); + QUnit.test( + 'Hide "Add follower" and subtypes edition/removal buttons except own user on read only record', + async function (assert) { + assert.expect(5); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsN( - document.body, - '.o_FollowerMenu_follower', - 2, - "Follower list should be refreshed and contain 2 followers" - ); - assert.strictEqual( - document.querySelector('.o_FollowerView_name').textContent, - "François Perusse", - "Follower added in follower list should be the one added" - ); -}); + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + ]); + pyEnv["mail.followers"].create([ + { + name: "Jean Michang", + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + name: "Eden Hazard", + is_active: true, + partner_id: resPartnerId2, + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with no write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = false; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); -QUnit.test('click on remove follower', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { name: 'resPartner1' }, - { name: 'resPartner2' }, - ]); - pyEnv['mail.followers'].create({ - partner_id: resPartnerId2, - email: "bla@bla.bla", - is_active: true, - name: "Wololo", - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = true; - return res; - } - if (route.includes('message_unsubscribe')) { - assert.step('message_unsubscribe'); - assert.deepEqual( - args.args, - [[resPartnerId1], [resPartnerId2]], - "message_unsubscribe should be called with right argument" + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_addFollowersButton", + "'Add followers' button should not be displayed for a readonly record" + ); + const followersList = document.querySelectorAll(".o_FollowerView"); + assert.containsOnce( + followersList[0], + ".o_FollowerView_editButton", + "should display edit button for a follower related to current user" + ); + assert.containsOnce( + followersList[0], + ".o_FollowerView_removeButton", + "should display remove button for a follower related to current user" + ); + assert.containsNone( + followersList[1], + ".o_FollowerView_editButton", + "should not display edit button for other followers on a readonly record" + ); + assert.containsNone( + followersList[1], + ".o_FollowerView_removeButton", + "should not display remove button for others on a readonly record" ); } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_removeButton', - "should display a remove button" - ); + QUnit.test( + 'Show "Add follower" and subtypes edition/removal buttons on all followers if user has write access', + async function (assert) { + assert.expect(5); - await click('.o_FollowerView_removeButton'); - assert.verifySteps( - ['message_unsubscribe'], - "clicking on remove button should call 'message_unsubscribe' route" - ); - assert.containsNone( - document.body, - '.o_FollowerView', - "should no longer have follower component" - ); -}); + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + ]); + pyEnv["mail.followers"].create([ + { + name: "Jean Michang", + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + name: "Eden Hazard", + is_active: true, + partner_id: resPartnerId2, + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); -QUnit.test('Hide "Add follower" and subtypes edition/removal buttons except own user on read only record', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{ name: "resPartner1" }, { name: "resPartner2" }]); - pyEnv['mail.followers'].create([ - { - name: "Jean Michang", - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - name: "Eden Hazard", - is_active: true, - partner_id: resPartnerId2, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with no write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = false; - return res; + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_addFollowersButton", + "'Add followers' button should be displayed for the writable record" + ); + const followersList = document.querySelectorAll(".o_FollowerView"); + assert.containsOnce( + followersList[0], + ".o_FollowerView_editButton", + "should display edit button for a follower related to current user" + ); + assert.containsOnce( + followersList[0], + ".o_FollowerView_removeButton", + "should display remove button for a follower related to current user" + ); + assert.containsOnce( + followersList[1], + ".o_FollowerView_editButton", + "should display edit button for other followers also on the writable record" + ); + assert.containsOnce( + followersList[1], + ".o_FollowerView_removeButton", + "should display remove button for other followers also on the writable record" + ); } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_addFollowersButton', - "'Add followers' button should not be displayed for a readonly record", - ); - const followersList = document.querySelectorAll('.o_FollowerView'); - assert.containsOnce( - followersList[0], - '.o_FollowerView_editButton', - "should display edit button for a follower related to current user", - ); - assert.containsOnce( - followersList[0], - '.o_FollowerView_removeButton', - "should display remove button for a follower related to current user", - ); - assert.containsNone( - followersList[1], - '.o_FollowerView_editButton', - "should not display edit button for other followers on a readonly record", - ); - assert.containsNone( - followersList[1], - '.o_FollowerView_removeButton', - "should not display remove button for others on a readonly record", - ); -}); + QUnit.test( + 'Show "No Followers" dropdown-item if there are no followers and user dose not have write access', + async function (assert) { + assert.expect(1); -QUnit.test('Show "Add follower" and subtypes edition/removal buttons on all followers if user has write access', async function (assert) { - assert.expect(5); + const pyEnv = await startServer(); - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{ name: "resPartner1" }, { name: "resPartner2" }]); - pyEnv['mail.followers'].create([ - { - name: "Jean Michang", - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - name: "Eden Hazard", - is_active: true, - partner_id: resPartnerId2, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user without write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = false; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_noFollowers.disabled", + "should display 'No Followers' dropdown-item" + ); } - }, + ); }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_addFollowersButton', - "'Add followers' button should be displayed for the writable record", - ); - const followersList = document.querySelectorAll('.o_FollowerView'); - assert.containsOnce( - followersList[0], - '.o_FollowerView_editButton', - "should display edit button for a follower related to current user", - ); - assert.containsOnce( - followersList[0], - '.o_FollowerView_removeButton', - "should display remove button for a follower related to current user", - ); - assert.containsOnce( - followersList[1], - '.o_FollowerView_editButton', - "should display edit button for other followers also on the writable record", - ); - assert.containsOnce( - followersList[1], - '.o_FollowerView_removeButton', - "should display remove button for other followers also on the writable record", - ); -}); - -QUnit.test('Show "No Followers" dropdown-item if there are no followers and user dose not have write access', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user without write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = false; - return res; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_noFollowers.disabled', - "should display 'No Followers' dropdown-item", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js index 2ad25d8946e..8aff4716323 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js @@ -1,169 +1,178 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follower_subtype_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follower_subtype_tests.js"); -QUnit.test('simplest layout of a followed subtype', async function (assert) { - assert.expect(5); + QUnit.test("simplest layout of a followed subtype", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const subtypeId = pyEnv['mail.message.subtype'].create({ - default: true, - name: 'TestSubtype', + const pyEnv = await startServer(); + const subtypeId = pyEnv["mail.message.subtype"].create({ + default: true, + name: "TestSubtype", + }); + const followerId = pyEnv["mail.followers"].create({ + display_name: "François Perusse", + partner_id: pyEnv.currentPartnerId, + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + subtype_ids: [subtypeId], + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { click, openView } = await start({ + // FIXME: should adapt mock server code to provide `hasWriteAccess` + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + await click(".o_FollowerView_editButton"); + assert.containsOnce( + document.body, + ".o_FollowerSubtypeView:contains(TestSubtype)", + "should have a follower subtype for 'TestSubtype'" + ); + assert.containsOnce( + document.querySelector(".o_FollowerSubtypeView"), + ".o_FollowerSubtypeView_label", + "should have a label" + ); + assert.containsOnce( + $(".o_FollowerSubtypeView:contains(TestSubtype)"), + ".o_FollowerSubtypeView_checkbox", + "should have a checkbox" + ); + assert.strictEqual( + $(".o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_label")[0] + .textContent, + "TestSubtype", + "should have the name of the subtype as label" + ); + assert.ok( + $(".o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox")[0] + .checked, + "checkbox should be checked as follower subtype is followed" + ); + }); + + QUnit.test("simplest layout of a not followed subtype", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + pyEnv["mail.message.subtype"].create({ + default: true, + name: "TestSubtype", + }); + const followerId = pyEnv["mail.followers"].create({ + display_name: "François Perusse", + partner_id: pyEnv.currentPartnerId, + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { click, openView } = await start({ + // FIXME: should adapt mock server code to provide `hasWriteAccess` + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + await click(".o_FollowerView_editButton"); + assert.notOk( + $(".o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox")[0] + .checked, + "checkbox should not be checked as follower subtype is not followed" + ); + }); + + QUnit.test("toggle follower subtype checkbox", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const followerSubtypeId = pyEnv["mail.message.subtype"].create({ + default: true, + name: "TestSubtype", + }); + const followerId = pyEnv["mail.followers"].create({ + display_name: "François Perusse", + partner_id: pyEnv.currentPartnerId, + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { click, openView } = await start({ + // FIXME: should adapt mock server code to provide `hasWriteAccess` + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + await click(".o_FollowerView_editButton"); + assert.notOk( + document.querySelector( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ).checked, + "checkbox should not be checked as follower subtype is not followed" + ); + + await click( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ); + assert.ok( + document.querySelector( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ).checked, + "checkbox should now be checked" + ); + + await click( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ); + assert.notOk( + document.querySelector( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ).checked, + "checkbox should be no more checked" + ); + }); }); - const followerId = pyEnv['mail.followers'].create({ - display_name: "François Perusse", - partner_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - subtype_ids: [subtypeId], - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { click, openView } = await start({ - // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - await click('.o_FollowerView_editButton'); - assert.containsOnce( - document.body, - '.o_FollowerSubtypeView:contains(TestSubtype)', - "should have a follower subtype for 'TestSubtype'" - ); - assert.containsOnce( - document.querySelector('.o_FollowerSubtypeView'), - '.o_FollowerSubtypeView_label', - "should have a label" - ); - assert.containsOnce( - $('.o_FollowerSubtypeView:contains(TestSubtype)'), - '.o_FollowerSubtypeView_checkbox', - "should have a checkbox" - ); - assert.strictEqual( - $('.o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_label')[0].textContent, - "TestSubtype", - "should have the name of the subtype as label" - ); - assert.ok( - $('.o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox')[0].checked, - "checkbox should be checked as follower subtype is followed" - ); -}); - -QUnit.test('simplest layout of a not followed subtype', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.message.subtype'].create({ - default: true, - name: 'TestSubtype', - }); - const followerId = pyEnv['mail.followers'].create({ - display_name: "François Perusse", - partner_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { click, openView } = await start({ - // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - await click('.o_FollowerView_editButton'); - assert.notOk( - $('.o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox')[0].checked, - "checkbox should not be checked as follower subtype is not followed" - ); -}); - -QUnit.test('toggle follower subtype checkbox', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const followerSubtypeId = pyEnv['mail.message.subtype'].create({ - default: true, - name: 'TestSubtype', - }); - const followerId = pyEnv['mail.followers'].create({ - display_name: "François Perusse", - partner_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { click, openView } = await start({ - // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - await click('.o_FollowerView_editButton'); - assert.notOk( - document.querySelector(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`).checked, - "checkbox should not be checked as follower subtype is not followed" - ); - - await click(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`); - assert.ok( - document.querySelector(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`).checked, - "checkbox should now be checked" - ); - - await click(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`); - assert.notOk( - document.querySelector(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`).checked, - "checkbox should be no more checked" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js index c898c16d7b3..46e1ede0458 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js @@ -1,295 +1,276 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { makeDeferred } from "@mail/utils/deferred"; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follower_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follower_tests.js"); -QUnit.test('base rendering not editable', async function (assert) { - assert.expect(5); + QUnit.test("base rendering not editable", async function (assert) { + assert.expect(5); - 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 { click, openView } = await start({ - async mockRPC(route, args, performRpc) { - if (route === '/mail/thread/data') { - // mimic user without write access - const res = await performRpc(...arguments); - res['hasWriteAccess'] = false; - return res; - } - }, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_details', - "should display a details part" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_avatar', - "should display the avatar of the follower" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_name', - "should display the name of the follower" - ); - assert.containsNone( - document.body, - '.o_FollowerView_button', - "should have no button as follower is not editable" - ); -}); - -QUnit.test('base rendering editable', async function (assert) { - assert.expect(6); - - 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 { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_details', - "should display a details part" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_avatar', - "should display the avatar of the follower" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_name', - "should display the name of the follower" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_editButton', - "should have an edit button" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_removeButton', - "should have a remove button" - ); -}); - -QUnit.test('click on partner follower details', async function (assert) { - assert.expect(7); - - 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 = makeDeferred(); - const { click, env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('do_action'); - assert.strictEqual( - action.res_id, - partnerId, - "The redirect action should redirect to the right res id (partnerId)" + 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 { click, openView } = await start({ + async mockRPC(route, args, performRpc) { + if (route === "/mail/thread/data") { + // mimic user without write access + const res = await performRpc(...arguments); + res["hasWriteAccess"] = false; + return res; + } + }, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_details", + "should display a details part" ); - assert.strictEqual( - action.res_model, - 'res.partner', - "The redirect action should redirect to the right res model (res.partner)" + assert.containsOnce( + document.body, + ".o_FollowerView_avatar", + "should display the avatar of the follower" ); - assert.strictEqual( - action.type, - "ir.actions.act_window", - "The redirect action should be of type 'ir.actions.act_window'" + assert.containsOnce( + document.body, + ".o_FollowerView_name", + "should display the name of the follower" ); - openFormDef.resolve(); - }, - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_details', - "should display a details part" - ); + assert.containsNone( + document.body, + ".o_FollowerView_button", + "should have no button as follower is not editable" + ); + }); - document.querySelector('.o_FollowerView_details').click(); - await openFormDef; - assert.verifySteps( - ['do_action'], - "clicking on follower should redirect to partner form view" - ); -}); - -QUnit.test('click on edit follower', async function (assert) { - assert.expect(5); - - 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 { click, messaging, openView } = await start({ - async mockRPC(route, args) { - if (route.includes('/mail/read_subscription_data')) { - assert.step('fetch_subtypes'); - } - }, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - const thread = messaging.models['Thread'].insert({ - id: threadId, - model: 'res.partner', - }); - await thread.fetchData(['followers']); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_editButton', - "should display an edit button" - ); - - await click('.o_FollowerView_editButton'); - assert.verifySteps( - ['fetch_subtypes'], - "clicking on edit follower should fetch subtypes" - ); - assert.containsOnce( - document.body, - '.o_FollowerSubtypeList', - "A dialog allowing to edit follower subtypes should have been created" - ); -}); - -QUnit.test('edit follower and close subtype dialog', async function (assert) { - assert.expect(6); - - 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 { click, openView } = await start({ - async mockRPC(route, args) { - if (route.includes('/mail/read_subscription_data')) { - assert.step('fetch_subtypes'); - return [{ - default: true, - followed: true, - internal: false, - id: 1, - name: "Dummy test", - res_model: 'res.partner' - }]; - } - }, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_editButton', - "should display an edit button" - ); - - await click('.o_FollowerView_editButton'); - assert.verifySteps( - ['fetch_subtypes'], - "clicking on edit follower should fetch subtypes" - ); - assert.containsOnce( - document.body, - '.o_FollowerSubtypeList', - "dialog allowing to edit follower subtypes should have been created" - ); - - await click('.o_FollowerSubtypeList_closeButton'); - assert.containsNone( - document.body, - '.o_DialogManager_dialog', - "follower subtype dialog should be closed after clicking on close button" - ); -}); - -}); + QUnit.test("base rendering editable", async function (assert) { + assert.expect(6); + + 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 { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_details", + "should display a details part" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_avatar", + "should display the avatar of the follower" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_name", + "should display the name of the follower" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_editButton", + "should have an edit button" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_removeButton", + "should have a remove button" + ); + }); + + QUnit.test("click on partner follower details", async function (assert) { + assert.expect(7); + + 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 = makeDeferred(); + const { click, env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("do_action"); + assert.strictEqual( + action.res_id, + partnerId, + "The redirect action should redirect to the right res id (partnerId)" + ); + assert.strictEqual( + action.res_model, + "res.partner", + "The redirect action should redirect to the right res model (res.partner)" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + "The redirect action should be of type 'ir.actions.act_window'" + ); + openFormDef.resolve(); + }, + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_details", + "should display a details part" + ); + + document.querySelector(".o_FollowerView_details").click(); + await openFormDef; + assert.verifySteps( + ["do_action"], + "clicking on follower should redirect to partner form view" + ); + }); + + QUnit.test("click on edit follower", async function (assert) { + assert.expect(5); + + 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 { click, messaging, openView } = await start({ + async mockRPC(route, args) { + if (route.includes("/mail/read_subscription_data")) { + assert.step("fetch_subtypes"); + } + }, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + const thread = messaging.models["Thread"].insert({ + id: threadId, + model: "res.partner", + }); + await thread.fetchData(["followers"]); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_editButton", + "should display an edit button" + ); + + await click(".o_FollowerView_editButton"); + assert.verifySteps( + ["fetch_subtypes"], + "clicking on edit follower should fetch subtypes" + ); + assert.containsOnce( + document.body, + ".o_FollowerSubtypeList", + "A dialog allowing to edit follower subtypes should have been created" + ); + }); + + QUnit.test("edit follower and close subtype dialog", async function (assert) { + assert.expect(6); + + 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 { click, openView } = await start({ + async mockRPC(route, args) { + if (route.includes("/mail/read_subscription_data")) { + assert.step("fetch_subtypes"); + return [ + { + default: true, + followed: true, + internal: false, + id: 1, + name: "Dummy test", + res_model: "res.partner", + }, + ]; + } + }, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_editButton", + "should display an edit button" + ); + + await click(".o_FollowerView_editButton"); + assert.verifySteps( + ["fetch_subtypes"], + "clicking on edit follower should fetch subtypes" + ); + assert.containsOnce( + document.body, + ".o_FollowerSubtypeList", + "dialog allowing to edit follower subtypes should have been created" + ); + + await click(".o_FollowerSubtypeList_closeButton"); + assert.containsNone( + document.body, + ".o_DialogManager_dialog", + "follower subtype dialog should be closed after clicking on close button" + ); + }); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js index aa3e65e7f1c..395d5a91910 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js @@ -1,384 +1,381 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('link_preview_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("link_preview_tests.js"); -const linkPreviewGifPayload = { - 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 linkPreviewCardPayload = { - og_description: 'Description', - og_title: 'Article title', - og_type: 'article', - source_url: 'https://www.odoo.com', -}; -const linkPreviewCardImagePayload = { - 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 linkPreviewVideoPayload = { - 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 linkPreviewImagePayload = { - 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 linkPreviewGifPayload = { + 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 linkPreviewCardPayload = { + og_description: "Description", + og_title: "Article title", + og_type: "article", + source_url: "https://www.odoo.com", + }; + const linkPreviewCardImagePayload = { + 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 linkPreviewVideoPayload = { + 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 linkPreviewImagePayload = { + 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", + }; -QUnit.test('auto layout with link preview list', async function (assert) { - assert.expect(1); + QUnit.test("auto layout with link preview list", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewGifPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView .o_LinkPreviewListView", + "Should have a link preview list in the DOM" + ); + }); + + QUnit.test("auto layout with link preview as gif", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewGifPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewImageView", + "Should have a link preview gif in the DOM" + ); + }); + + QUnit.test("simplest card layout", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewCardPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView", + "should have link preview in DOM" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_title", + "Should display the link preview title" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_description", + "Link preview should show the link description" + ); + }); + + QUnit.test("simplest card layout with image", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewCardImagePayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView", + "should have link preview in DOM" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_title", + "Should display the link preview title" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_description", + "Link preview should show the link description" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_image", + "Should display an image inside the link preview card" + ); + }); + + QUnit.test("Link preview video layout", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewVideoPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewVideoView", + "should have link preview video in DOM" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewVideoView_title", + "Should display the link preview title" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewVideoView_description", + "Link preview should show the link description" + ); + assert.containsOnce( + document.body, + ".o_linkPreviewVideo_overlay", + "Should display overlay inside the link preview video image" + ); + }); + + QUnit.test("Link preview image layout", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewImagePayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewImageView", + "should have link preview image" + ); + }); + + QUnit.test("Remove link preview Gif", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewGifPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewImageView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); + + QUnit.test("Remove link preview card", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewCardPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewCardView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); + + QUnit.test("Remove link preview video", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewVideoPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewVideoView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); + + QUnit.test("Remove link preview image", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewImagePayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewImageView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView .o_LinkPreviewListView', - "Should have a link preview list in the DOM" - ); -}); - -QUnit.test('auto layout with link preview as gif', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewImageView', - "Should have a link preview gif in the DOM" - ); -}); - -QUnit.test('simplest card layout', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView', - "should have link preview in DOM" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_title', - "Should display the link preview title" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_description', - "Link preview should show the link description" - ); -}); - -QUnit.test('simplest card layout with image', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardImagePayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView', - "should have link preview in DOM" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_title', - "Should display the link preview title" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_description', - "Link preview should show the link description" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_image', - "Should display an image inside the link preview card" - ); -}); - -QUnit.test('Link preview video layout', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewVideoView', - "should have link preview video in DOM" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewVideoView_title', - "Should display the link preview title" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewVideoView_description', - "Link preview should show the link description" - ); - assert.containsOnce( - document.body, - '.o_linkPreviewVideo_overlay', - "Should display overlay inside the link preview video image" - ); -}); - -QUnit.test('Link preview image layout', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewImageView', - "should have link preview image" - ); -}); - -QUnit.test('Remove link preview Gif', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewImageView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -QUnit.test('Remove link preview card', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewCardView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -QUnit.test('Remove link preview video', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewVideoView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -QUnit.test('Remove link preview image', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewImageView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js index a644812cd44..d278f29fea9 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js @@ -1,43 +1,48 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function() { -QUnit.module('message_in_reply_to_view_tests'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("message_in_reply_to_view_tests"); -QUnit.test('click on message in reply to highlights the parent message', async function (assert) { - assert.expect(1); + QUnit.test( + "click on message in reply to highlights the parent message", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "Hey lol", - message_type: 'comment', - model: 'mail.channel', - res_id: mailChannelId1, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "Hey lol", + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId1, + }); + const mailMessageId2 = pyEnv["mail.message"].create({ + body: "Response to Hey lol", + message_type: "comment", + model: "mail.channel", + parent_id: mailMessageId1, + res_id: mailChannelId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click( + `.o_MessageView[data-id="${mailMessageId2}"] .o_MessageInReplyToView_body` + ); + assert.containsOnce( + document.body, + `.o-highlighted[data-id="${mailMessageId1}"]`, + "click on message in reply to should highlight the parent message" + ); + } + ); }); - const mailMessageId2 = pyEnv['mail.message'].create({ - body: "Response to Hey lol", - message_type: 'comment', - model: 'mail.channel', - parent_id: mailMessageId1, - res_id: mailChannelId1, - }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click(`.o_MessageView[data-id="${mailMessageId2}"] .o_MessageInReplyToView_body`); - assert.containsOnce( - document.body, - `.o-highlighted[data-id="${mailMessageId1}"]`, - "click on message in reply to should highlight the parent message" - ); -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js index 506a8fe8332..06521587fe8 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js @@ -1,271 +1,285 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('message_seen_indicator_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("message_seen_indicator_tests.js"); -QUnit.test('rendering when just one has received the message', async function (assert) { - assert.expect(3); + QUnit.test("rendering when just one has received the message", async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', // only chat channel have seen notification + const pyEnv = await startServer(); + + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", // only chat channel have seen notification + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const [mailChannelMemberId1] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ["partner_id", "=", resPartnerId1], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId1], { + fetched_message_id: mailMessageId, + seen_message_id: false, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "should display only one seen indicator icon" + ); + }); + + QUnit.test("rendering when everyone have received the message", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const mailChannelMemberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(mailChannelMemberIds, { + fetched_message_id: mailMessageId, + seen_message_id: false, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "should display only one seen indicator icon" + ); + }); + + QUnit.test("rendering when just one has seen the message", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const mailChannelMemberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(mailChannelMemberIds, { + fetched_message_id: mailMessageId, + seen_message_id: false, + }); + const [mailChannelMemberId1] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ["partner_id", "=", resPartnerId1], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId1], { + seen_message_id: mailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "should display two seen indicator icon" + ); + }); + + QUnit.test( + "rendering when just one has seen & received the message", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const [mailChannelMemberId1] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ["partner_id", "=", resPartnerId1], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId1], { + seen_message_id: mailMessageId, + fetched_message_id: mailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "should display two seen indicator icon" + ); + } + ); + + QUnit.test("rendering when just everyone has seen the message", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const mailChannelMemberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(mailChannelMemberIds, { + fetched_message_id: mailMessageId, + seen_message_id: mailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.hasClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not considered as all seen" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "should display two seen indicator icon" + ); + }); }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId1], { - fetched_message_id: mailMessageId, - seen_message_id: false, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "should display only one seen indicator icon" - ); -}); - -QUnit.test('rendering when everyone have received the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(mailChannelMemberIds, { - fetched_message_id: mailMessageId, - seen_message_id: false, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "should display only one seen indicator icon" - ); -}); - -QUnit.test('rendering when just one has seen the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(mailChannelMemberIds, { - fetched_message_id: mailMessageId, - seen_message_id: false, - }); - const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId1], { - seen_message_id: mailMessageId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "should display two seen indicator icon" - ); -}); - -QUnit.test('rendering when just one has seen & received the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId1], { - seen_message_id: mailMessageId, - fetched_message_id: mailMessageId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "should display two seen indicator icon" - ); -}); - -QUnit.test('rendering when just everyone has seen the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(mailChannelMemberIds, { - fetched_message_id: mailMessageId, - seen_message_id: mailMessageId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.hasClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not considered as all seen" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "should display two seen indicator icon" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js index 07b785fed7c..d96dcab2fc9 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js @@ -1,1095 +1,1184 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; +import { makeDeferred } from "@mail/utils/deferred"; import { afterNextRender, nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('message_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("message_tests.js"); -QUnit.test('basic rendering', async function (assert) { - assert.expect(12); + QUnit.test("basic rendering", async function (assert) { + assert.expect(12); - const pyEnv = await startServer(); - const [threadId, otherPartnerId] = pyEnv['res.partner'].create([{}, { name: 'Demo User' }]); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: otherPartnerId, - body: 'Test
', - model: 'res.partner', - res_id: threadId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should display a message component" - ); - const messageEl = document.querySelector('.o_MessageView'); - assert.strictEqual( - messageEl.dataset.id, - mailMessageId.toString(), - "message component should be linked to message store model" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_sidebar`).length, - 1, - "message should have a sidebar" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_sidebar .o_MessageView_authorAvatar`).length, - 1, - "message should have author avatar in the sidebar" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).tagName, - 'IMG', - "message author avatar should be an image" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, - `/web/image/res.partner/${otherPartnerId}/avatar_128`, - "message author avatar should GET image of the related partner" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_authorName`).length, - 1, - "message should display author name" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_authorName`).textContent, - "Demo User", - "message should display correct author name" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_date`).length, - 1, - "message should display date" - ); - await afterNextRender(() => messageEl.click()); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageActionList`).length, - 1, - "message should display list of actions" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_content`).length, - 1, - "message should display the content" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_prettyBody`).innerHTML, - "Test
", - "message should display the correct content" - ); -}); - -QUnit.test('Notification Sent', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, { name: 'Someone', partner_share: true }]); - const mailMessageId = pyEnv['mail.message'].create({ - body: 'not empty', - message_type: 'email', - model: 'res.partner', - res_id: threadId, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId, - notification_status: 'sent', - notification_type: 'email', - res_partner_id: resPartnerId, - }); - const { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIconClickable', - "should display the notification icon container" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIcon', - "should display the notification icon" - ); - assert.hasClass( - document.querySelector('.o_MessageView_notificationIcon'), - 'fa-envelope-o', - "icon should represent email success" - ); - - await click('.o_MessageView_notificationIconClickable'); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView', - "notification popover should be open" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationIcon', - "popover should have one icon" - ); - assert.hasClass( - document.querySelector('.o_MessageNotificationPopoverContentView_notificationIcon'), - 'fa-check', - "popover should have the sent icon" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationPartnerName', - "popover should have the partner name" - ); - assert.strictEqual( - document.querySelector('.o_MessageNotificationPopoverContentView_notificationPartnerName').textContent.trim(), - "Someone", - "partner name should be correct" - ); -}); - -QUnit.test('Notification Error', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, { name: "Someone", partner_share: true }]); - const mailMessageId = pyEnv['mail.message'].create({ - body: 'not empty', - message_type: 'email', - model: 'res.partner', - res_id: threadId, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId, - notification_status: 'exception', - notification_type: 'email', - res_partner_id: resPartnerId, - }); - const openResendActionDef = makeDeferred(); - const { env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action, options) { - assert.step('do_action'); + const pyEnv = await startServer(); + const [threadId, otherPartnerId] = pyEnv["res.partner"].create([ + {}, + { name: "Demo User" }, + ]); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: otherPartnerId, + body: "Test
", + model: "res.partner", + res_id: threadId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); assert.strictEqual( - action, - 'mail.mail_resend_message_action', - "action should be the one to resend email" + document.querySelectorAll(".o_MessageView").length, + 1, + "should display a message component" + ); + const messageEl = document.querySelector(".o_MessageView"); + assert.strictEqual( + messageEl.dataset.id, + mailMessageId.toString(), + "message component should be linked to message store model" ); assert.strictEqual( - options.additionalContext.mail_message_to_resend, - mailMessageId, - "action should have correct message id" + messageEl.querySelectorAll(`:scope .o_MessageView_sidebar`).length, + 1, + "message should have a sidebar" + ); + assert.strictEqual( + messageEl.querySelectorAll( + `:scope .o_MessageView_sidebar .o_MessageView_authorAvatar` + ).length, + 1, + "message should have author avatar in the sidebar" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).tagName, + "IMG", + "message author avatar should be an image" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, + `/web/image/res.partner/${otherPartnerId}/avatar_128`, + "message author avatar should GET image of the related partner" + ); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageView_authorName`).length, + 1, + "message should display author name" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_authorName`).textContent, + "Demo User", + "message should display correct author name" + ); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageView_date`).length, + 1, + "message should display date" + ); + await afterNextRender(() => messageEl.click()); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageActionList`).length, + 1, + "message should display list of actions" + ); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageView_content`).length, + 1, + "message should display the content" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_prettyBody`).innerHTML, + "Test
", + "message should display the correct content" ); - openResendActionDef.resolve(); - }, - }); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIconClickable', - "should display the notification icon container" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIcon', - "should display the notification icon" - ); - assert.hasClass( - document.querySelector('.o_MessageView_notificationIcon'), - 'fa-envelope', - "icon should represent email error" - ); - document.querySelector('.o_MessageView_notificationIconClickable').click(); - await openResendActionDef; - assert.verifySteps( - ['do_action'], - "should do an action to display the resend email dialog" - ); -}); - -QUnit.test("'channel_fetch' notification received is correctly handled", async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message component should not have any check (V) as message is not yet received" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId]])[0]; - // Simulate received channel fetched notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/fetched', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, }); - }); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message seen indicator component should only contain one check (V) as message is just received" - ); -}); + QUnit.test("Notification Sent", async function (assert) { + assert.expect(9); -QUnit.test("'channel_seen' notification received is correctly handled", async function (assert) { - assert.expect(3); + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([ + {}, + { name: "Someone", partner_share: true }, + ]); + const mailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + message_type: "email", + model: "res.partner", + res_id: threadId, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId, + notification_status: "sent", + notification_type: "email", + res_partner_id: resPartnerId, + }); + const { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIconClickable", + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIcon", + "should display the notification icon" + ); + assert.hasClass( + document.querySelector(".o_MessageView_notificationIcon"), + "fa-envelope-o", + "icon should represent email success" + ); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message component should not have any check (V) as message is not yet received" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId]])[0]; - // Simulate received channel seen notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/seen', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, - }); - }); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "message seen indicator component should contain two checks (V) as message is seen" - ); -}); - -QUnit.test("'channel_fetch' notification then 'channel_seen' received are correctly handled", async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ display_name: "Recipient" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message component should not have any check (V) as message is not yet received" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId]])[0]; - // Simulate received channel fetched notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/fetched', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, - }); - }); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message seen indicator component should only contain one check (V) as message is just received" - ); - - // Simulate received channel seen notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/seen', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, - }); - }); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "message seen indicator component should contain two checks (V) as message is now seen" - ); -}); - -QUnit.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 function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const otherPartnerId = pyEnv['res.partner'].create({ name: 'Demo User' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_type: 'chat', - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: otherPartnerId }], - ], - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: otherPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const memberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(memberIds, { seen_message_id: mailMessageId }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageView_seenIndicator', - "message component should not have any message seen indicator because the current user is not author" - ); -}); - -QUnit.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 function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const otherPartnerId = pyEnv['res.partner'].create({ name: 'Demo User' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_type: 'chat', - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: otherPartnerId }], - ], - }); - const [beforeLastMailMessageId, lastMailMessageId] = pyEnv['mail.message'].create([ - { - author_id: pyEnv.currentPartnerId, - body: "Message before last seen
", - model: 'mail.channel', - res_id: mailChannelId, - }, - { - author_id: pyEnv.currentPartnerId, - body: "Last seen by everyone
", - model: 'mail.channel', - res_id: mailChannelId, - }, - ]); - const memberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(memberIds, { seen_message_id: lastMailMessageId }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_MessageView[data-id=${beforeLastMailMessageId}]`, - "should display a message component" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageView_seenIndicator`, - "message component should have a message seen indicator because the current user is author" - ); - assert.containsNone( - document.body, - `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageSeenIndicatorView_icon`, - "message component should not have any check (V) because it is older than the last message seen by everyone" - ); -}); - -QUnit.test('only show messaging seen indicator if authored by me, after last seen by all message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const otherPartnerId = pyEnv['res.partner'].create({ name: 'Demo User' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_type: 'chat', - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: otherPartnerId }], - ], - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "Test
", - res_id: mailChannelId, - model: 'mail.channel', - }); - const memberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(memberIds, { - fetched_message_id: mailMessageId, - seen_message_id: mailMessageId - 1, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsOnce( - document.body, - '.o_MessageView_seenIndicator', - "message component should have a message seen indicator" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 1, - "message component should have one check (V) because the message was fetched by everyone but no other member than author has seen the message" - ); -}); - -QUnit.test('allow attachment delete on authored message', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - attachment_ids: [[0, 0, { - mimetype: 'image/jpeg', - name: "BLAH", - res_id: mailChannelId, - res_model: 'mail.channel', - }]], - author_id: pyEnv.currentPartnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage', - "should have an attachment", - ); - assert.containsOnce( - document.body, - '.o_AttachmentImage_actionUnlink', - "should have delete attachment button" - ); - - await click('.o_AttachmentImage_actionUnlink'); - assert.containsOnce( - document.body, - '.o_AttachmentDeleteConfirmView', - "An attachment delete confirmation dialog should have been opened" - ); - assert.strictEqual( - document.querySelector('.o_AttachmentDeleteConfirmView_mainText').textContent, - `Do you really want to delete "BLAH"?`, - "Confirmation dialog should contain the attachment delete confirmation text" - ); - - await click('.o_AttachmentDeleteConfirmView_confirmButton'); - assert.containsNone( - document.body, - '.o_AttachmentCard', - "should no longer have an attachment", - ); -}); - -QUnit.test('prevent attachment delete on non-authored message in channels', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({}); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - attachment_ids: [[0, 0, { - mimetype: 'image/jpeg', - name: "BLAH", - res_id: mailChannelId, - res_model: 'mail.channel', - }]], - author_id: partnerId, - body: "Test
", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage', - "should have an attachment", - ); - assert.containsNone( - document.body, - '.o_AttachmentImage_actionUnlink', - "delete attachment button should not be printed" - ); -}); - -QUnit.test('allow attachment image download on message', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const irAttachmentId1 = pyEnv['ir.attachment'].create({ - name: "Blah.jpg", - mimetype: 'image/jpeg', - }); - pyEnv['mail.message'].create({ - attachment_ids: [irAttachmentId1], - body: 'Test
', - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_AttachmentImage_actionDownload', - "should have download attachment button" - ); -}); - -QUnit.test('subtype description should be displayed if it is different than body', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const subtypeId = pyEnv['mail.message.subtype'].create({ description: "Bonjour" }); - pyEnv['mail.message'].create({ - body: "Hello
", - model: 'res.partner', - res_id: threadId, - subtype_id: subtypeId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView_content', - "message should have content" - ); - assert.strictEqual( - document.querySelector(`.o_MessageView_content`).textContent, - "HelloBonjour", - "message content should display both body and subtype description when they are different" - ); -}); - -QUnit.test('subtype description should not be displayed if it is similar to body', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const subtypeId = pyEnv['mail.message.subtype'].create({ description: "hello" }); - pyEnv['mail.message'].create({ - body: "Hello
", - model: 'res.partner', - res_id: threadId, - subtype_id: subtypeId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView_content', - "message should have content" - ); - assert.strictEqual( - document.querySelector(`.o_MessageView_content`).textContent, - "Hello", - "message content should display only body when subtype description is similar" - ); -}); - -QUnit.test('data-oe-id & data-oe-model link redirection on click', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - pyEnv['mail.message'].create({ - body: ``, - model: 'res.partner', - res_id: threadId, - }); - const { env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.strictEqual( - action.type, - 'ir.actions.act_window', - "action should open view" + await click(".o_MessageView_notificationIconClickable"); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView", + "notification popover should be open" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationIcon", + "popover should have one icon" + ); + assert.hasClass( + document.querySelector(".o_MessageNotificationPopoverContentView_notificationIcon"), + "fa-check", + "popover should have the sent icon" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationPartnerName", + "popover should have the partner name" ); assert.strictEqual( - action.res_model, - 'some.model', - "action should open view on 'some.model' model" + document + .querySelector( + ".o_MessageNotificationPopoverContentView_notificationPartnerName" + ) + .textContent.trim(), + "Someone", + "partner name should be correct" + ); + }); + + QUnit.test("Notification Error", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([ + {}, + { name: "Someone", partner_share: true }, + ]); + const mailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + message_type: "email", + model: "res.partner", + res_id: threadId, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId, + notification_status: "exception", + notification_type: "email", + res_partner_id: resPartnerId, + }); + const openResendActionDef = makeDeferred(); + const { env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { + assert.step("do_action"); + assert.strictEqual( + action, + "mail.mail_resend_message_action", + "action should be the one to resend email" + ); + assert.strictEqual( + options.additionalContext.mail_message_to_resend, + mailMessageId, + "action should have correct message id" + ); + openResendActionDef.resolve(); + }, + }); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIconClickable", + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIcon", + "should display the notification icon" + ); + assert.hasClass( + document.querySelector(".o_MessageView_notificationIcon"), + "fa-envelope", + "icon should represent email error" + ); + document.querySelector(".o_MessageView_notificationIconClickable").click(); + await openResendActionDef; + assert.verifySteps( + ["do_action"], + "should do an action to display the resend email dialog" + ); + }); + + QUnit.test( + "'channel_fetch' notification received is correctly handled", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message component should not have any check (V) as message is not yet received" + ); + + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId], + ])[0]; + // Simulate received channel fetched notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/fetched", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message seen indicator component should only contain one check (V) as message is just received" + ); + } + ); + + QUnit.test( + "'channel_seen' notification received is correctly handled", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message component should not have any check (V) as message is not yet received" + ); + + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId], + ])[0]; + // Simulate received channel seen notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/seen", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "message seen indicator component should contain two checks (V) as message is seen" + ); + } + ); + + QUnit.test( + "'channel_fetch' notification then 'channel_seen' received are correctly handled", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ display_name: "Recipient" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message component should not have any check (V) as message is not yet received" + ); + + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId], + ])[0]; + // Simulate received channel fetched notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/fetched", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message seen indicator component should only contain one check (V) as message is just received" + ); + + // Simulate received channel seen notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/seen", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "message seen indicator component should contain two checks (V) as message is now seen" + ); + } + ); + + QUnit.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 function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const otherPartnerId = pyEnv["res.partner"].create({ name: "Demo User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_type: "chat", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: otherPartnerId }], + ], + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: otherPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const memberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(memberIds, { seen_message_id: mailMessageId }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageView_seenIndicator", + "message component should not have any message seen indicator because the current user is not author" + ); + } + ); + + QUnit.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 function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const otherPartnerId = pyEnv["res.partner"].create({ name: "Demo User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_type: "chat", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: otherPartnerId }], + ], + }); + const [beforeLastMailMessageId, lastMailMessageId] = pyEnv["mail.message"].create([ + { + author_id: pyEnv.currentPartnerId, + body: "Message before last seen
", + model: "mail.channel", + res_id: mailChannelId, + }, + { + author_id: pyEnv.currentPartnerId, + body: "Last seen by everyone
", + model: "mail.channel", + res_id: mailChannelId, + }, + ]); + const memberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(memberIds, { + seen_message_id: lastMailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_MessageView[data-id=${beforeLastMailMessageId}]`, + "should display a message component" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageView_seenIndicator`, + "message component should have a message seen indicator because the current user is author" + ); + assert.containsNone( + document.body, + `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageSeenIndicatorView_icon`, + "message component should not have any check (V) because it is older than the last message seen by everyone" + ); + } + ); + + QUnit.test( + "only show messaging seen indicator if authored by me, after last seen by all message", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const otherPartnerId = pyEnv["res.partner"].create({ name: "Demo User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_type: "chat", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: otherPartnerId }], + ], + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "Test
", + res_id: mailChannelId, + model: "mail.channel", + }); + const memberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(memberIds, { + fetched_message_id: mailMessageId, + seen_message_id: mailMessageId - 1, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsOnce( + document.body, + ".o_MessageView_seenIndicator", + "message component should have a message seen indicator" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 1, + "message component should have one check (V) because the message was fetched by everyone but no other member than author has seen the message" + ); + } + ); + + QUnit.test("allow attachment delete on authored message", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [ + [ + 0, + 0, + { + mimetype: "image/jpeg", + name: "BLAH", + res_id: mailChannelId, + res_model: "mail.channel", + }, + ], + ], + author_id: pyEnv.currentPartnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce(document.body, ".o_AttachmentImage", "should have an attachment"); + assert.containsOnce( + document.body, + ".o_AttachmentImage_actionUnlink", + "should have delete attachment button" + ); + + await click(".o_AttachmentImage_actionUnlink"); + assert.containsOnce( + document.body, + ".o_AttachmentDeleteConfirmView", + "An attachment delete confirmation dialog should have been opened" ); assert.strictEqual( - action.res_id, - 250, - "action should open view on 250" + document.querySelector(".o_AttachmentDeleteConfirmView_mainText").textContent, + `Do you really want to delete "BLAH"?`, + "Confirmation dialog should contain the attachment delete confirmation text" ); - assert.step('do-action:openFormView_some.model_250'); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView_content', - "message should have content" - ); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - 'a', - "message content should have a link" - ); - document.querySelector(`.o_MessageView_content a`).click(); - assert.verifySteps( - ['do-action:openFormView_some.model_250'], - "should have open form view on related record after click on link" - ); -}); - -QUnit.test('chat with author should be opened after clicking on their avatar', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'res.partner', - res_id: threadId, - }); - const { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView_authorAvatar', - "message should have the author avatar" - ); - assert.hasClass( - document.querySelector('.o_MessageView_authorAvatar'), - 'o_redirect', - "author avatar should have the redirect style" - ); - - await click('.o_MessageView_authorAvatar'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on author avatar" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with author should be opened after clicking on their avatar" - ); -}); - -QUnit.test('chat with author should be opened after clicking on their name', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'res.partner', - res_id: resPartnerId, - }); - const { click, openFormView } = await start(); - await openFormView({ - res_model: 'res.partner', - res_id: resPartnerId, - }); - assert.containsOnce( - document.body, - '.o_MessageView_authorName', - "message should have the author name" - ); - assert.hasClass( - document.querySelector('.o_MessageView_authorName'), - 'o_redirect', - "author name should have the redirect style" - ); - - await click('.o_MessageView_authorName'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on author name" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with author should be opened after clicking on their name" - ); -}); - -QUnit.test('chat with author should be opened after clicking on their im status icon', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, { im_status: 'online' }]); - pyEnv['res.users'].create({ - im_status: 'online', - partner_id: resPartnerId, - }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'res.partner', - res_id: threadId, - }); - const { advanceTime, click, openView } = await start({ - hasTimeControl: true, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await afterNextRender(() => advanceTime(50 * 1000)); // next fetch of im_status - assert.containsOnce( - document.body, - '.o_MessageView_personaImStatusIcon', - "message should have the author im status icon" - ); - assert.hasClass( - document.querySelector('.o_MessageView_personaImStatusIcon'), - 'o-has-open-chat', - "author im status icon should have the open chat style" - ); - - await click('.o_MessageView_personaImStatusIcon'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on author im status icon" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with author should be opened after clicking on their im status icon" - ); -}); - -QUnit.test('open chat with author on avatar click should be disabled when currently chatting with the author', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageView_authorAvatar', - "message should have the author avatar" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageView_authorAvatar'), - 'o_redirect', - "author avatar should not have the redirect style" - ); - - document.querySelector('.o_MessageView_authorAvatar').click(); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have no thread opened after clicking on author avatar when currently chatting with the author" - ); -}); - -QUnit.test('Chat with partner should be opened after clicking on their mention', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ name: 'Test Partner', email: 'testpartner@odoo.com' }); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - const { click, insertText, openFormView } = await start(); - await openFormView({ - res_model: 'res.partner', - res_id: resPartnerId, - }); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - await click('.o_mail_redirect'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on partner mention" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with partner should be opened after clicking on their mention" - ); -}); - -QUnit.test('Channel should be opened after clicking on its mention', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { click, insertText, openFormView } = await start(); - await openFormView({ - res_model: 'res.partner', - res_id: resPartnerId, - }); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "#my-channel"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - await click('.o_channel_redirect'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on channel mention" - ); -}); - -QUnit.test('message should not be considered as "clicked" after clicking on its author avatar', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: "Test
", - model: 'res.partner', - res_id: threadId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - document.querySelector(`.o_MessageView_authorAvatar`).click(); - await nextAnimationFrame(); - assert.doesNotHaveClass( - document.querySelector(`.o_MessageView`), - 'o-clicked', - "message should not be considered as 'clicked' after clicking on its author avatar" - ); -}); - -QUnit.test('message should not be considered as "clicked" after clicking on notification failure icon', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const mailMessageId = pyEnv['mail.message'].create({ - body: 'not empty', - model: 'res.partner', - res_id: threadId, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId, - notification_status: 'exception', - notification_type: 'email', - }); - const { env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - // intercept the action: this action is not relevant in the context of this test. - doAction() {}, - }); - document.querySelector('.o_MessageView_notificationIconClickable.o-error').click(); - await nextAnimationFrame(); - assert.doesNotHaveClass( - document.querySelector(`.o_MessageView`), - 'o-clicked', - "message should not be considered as 'clicked' after clicking on notification failure icon" - ); -}); - -}); + await click(".o_AttachmentDeleteConfirmView_confirmButton"); + assert.containsNone( + document.body, + ".o_AttachmentCard", + "should no longer have an attachment" + ); + }); + + QUnit.test( + "prevent attachment delete on non-authored message in channels", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({}); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [ + [ + 0, + 0, + { + mimetype: "image/jpeg", + name: "BLAH", + res_id: mailChannelId, + res_model: "mail.channel", + }, + ], + ], + author_id: partnerId, + body: "Test
", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_AttachmentImage", + "should have an attachment" + ); + assert.containsNone( + document.body, + ".o_AttachmentImage_actionUnlink", + "delete attachment button should not be printed" + ); + } + ); + + QUnit.test("allow attachment image download on message", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const irAttachmentId1 = pyEnv["ir.attachment"].create({ + name: "Blah.jpg", + mimetype: "image/jpeg", + }); + pyEnv["mail.message"].create({ + attachment_ids: [irAttachmentId1], + body: "Test
", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_AttachmentImage_actionDownload", + "should have download attachment button" + ); + }); + + QUnit.test( + "subtype description should be displayed if it is different than body", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const subtypeId = pyEnv["mail.message.subtype"].create({ description: "Bonjour" }); + pyEnv["mail.message"].create({ + body: "Hello
", + model: "res.partner", + res_id: threadId, + subtype_id: subtypeId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "message should have content" + ); + assert.strictEqual( + document.querySelector(`.o_MessageView_content`).textContent, + "HelloBonjour", + "message content should display both body and subtype description when they are different" + ); + } + ); + + QUnit.test( + "subtype description should not be displayed if it is similar to body", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const subtypeId = pyEnv["mail.message.subtype"].create({ description: "hello" }); + pyEnv["mail.message"].create({ + body: "Hello
", + model: "res.partner", + res_id: threadId, + subtype_id: subtypeId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "message should have content" + ); + assert.strictEqual( + document.querySelector(`.o_MessageView_content`).textContent, + "Hello", + "message content should display only body when subtype description is similar" + ); + } + ); + + QUnit.test("data-oe-id & data-oe-model link redirection on click", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + pyEnv["mail.message"].create({ + body: ``, + model: "res.partner", + res_id: threadId, + }); + const { env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.strictEqual( + action.type, + "ir.actions.act_window", + "action should open view" + ); + assert.strictEqual( + action.res_model, + "some.model", + "action should open view on 'some.model' model" + ); + assert.strictEqual(action.res_id, 250, "action should open view on 250"); + assert.step("do-action:openFormView_some.model_250"); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "message should have content" + ); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + "a", + "message content should have a link" + ); + + document.querySelector(`.o_MessageView_content a`).click(); + assert.verifySteps( + ["do-action:openFormView_some.model_250"], + "should have open form view on related record after click on link" + ); + }); + + QUnit.test( + "chat with author should be opened after clicking on their avatar", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "res.partner", + res_id: threadId, + }); + const { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView_authorAvatar", + "message should have the author avatar" + ); + assert.hasClass( + document.querySelector(".o_MessageView_authorAvatar"), + "o_redirect", + "author avatar should have the redirect style" + ); + + await click(".o_MessageView_authorAvatar"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on author avatar" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with author should be opened after clicking on their avatar" + ); + } + ); + + QUnit.test( + "chat with author should be opened after clicking on their name", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "res.partner", + res_id: resPartnerId, + }); + const { click, openFormView } = await start(); + await openFormView({ + res_model: "res.partner", + res_id: resPartnerId, + }); + assert.containsOnce( + document.body, + ".o_MessageView_authorName", + "message should have the author name" + ); + assert.hasClass( + document.querySelector(".o_MessageView_authorName"), + "o_redirect", + "author name should have the redirect style" + ); + + await click(".o_MessageView_authorName"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on author name" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with author should be opened after clicking on their name" + ); + } + ); + + QUnit.test( + "chat with author should be opened after clicking on their im status icon", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([ + {}, + { im_status: "online" }, + ]); + pyEnv["res.users"].create({ + im_status: "online", + partner_id: resPartnerId, + }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "res.partner", + res_id: threadId, + }); + const { advanceTime, click, openView } = await start({ + hasTimeControl: true, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await afterNextRender(() => advanceTime(50 * 1000)); // next fetch of im_status + assert.containsOnce( + document.body, + ".o_MessageView_personaImStatusIcon", + "message should have the author im status icon" + ); + assert.hasClass( + document.querySelector(".o_MessageView_personaImStatusIcon"), + "o-has-open-chat", + "author im status icon should have the open chat style" + ); + + await click(".o_MessageView_personaImStatusIcon"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on author im status icon" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with author should be opened after clicking on their im status icon" + ); + } + ); + + QUnit.test( + "open chat with author on avatar click should be disabled when currently chatting with the author", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageView_authorAvatar", + "message should have the author avatar" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageView_authorAvatar"), + "o_redirect", + "author avatar should not have the redirect style" + ); + + document.querySelector(".o_MessageView_authorAvatar").click(); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have no thread opened after clicking on author avatar when currently chatting with the author" + ); + } + ); + + QUnit.test( + "Chat with partner should be opened after clicking on their mention", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + name: "Test Partner", + email: "testpartner@odoo.com", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + const { click, insertText, openFormView } = await start(); + await openFormView({ + res_model: "res.partner", + res_id: resPartnerId, + }); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + await click(".o_mail_redirect"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on partner mention" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with partner should be opened after clicking on their mention" + ); + } + ); + + QUnit.test( + "Channel should be opened after clicking on its mention", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + pyEnv["mail.channel"].create({ name: "my-channel" }); + const { click, insertText, openFormView } = await start(); + await openFormView({ + res_model: "res.partner", + res_id: resPartnerId, + }); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "#my-channel"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + await click(".o_channel_redirect"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on channel mention" + ); + } + ); + + QUnit.test( + 'message should not be considered as "clicked" after clicking on its author avatar', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "Test
", + model: "res.partner", + res_id: threadId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + document.querySelector(`.o_MessageView_authorAvatar`).click(); + await nextAnimationFrame(); + assert.doesNotHaveClass( + document.querySelector(`.o_MessageView`), + "o-clicked", + "message should not be considered as 'clicked' after clicking on its author avatar" + ); + } + ); + + QUnit.test( + 'message should not be considered as "clicked" after clicking on notification failure icon', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const mailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: threadId, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId, + notification_status: "exception", + notification_type: "email", + }); + const { env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + // intercept the action: this action is not relevant in the context of this test. + doAction() {}, + }); + document.querySelector(".o_MessageView_notificationIconClickable.o-error").click(); + await nextAnimationFrame(); + assert.doesNotHaveClass( + document.querySelector(`.o_MessageView`), + "o-clicked", + "message should not be considered as 'clicked' after clicking on notification failure icon" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js index 01b6f1e53fd..e773886738e 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js @@ -1,914 +1,998 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { browser } from '@web/core/browser/browser'; +import { browser } from "@web/core/browser/browser"; import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; -import { makeTestPromise } from 'web.test_utils'; +import { makeTestPromise } from "web.test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('messaging_menu_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("messaging_menu_tests.js"); -QUnit.test('[technical] messaging not created then becomes created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(2); + QUnit.test( + "[technical] messaging not created then becomes created", + async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(2); - const messagingBeforeCreationDeferred = makeTestPromise(); - await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', - }); - assert.containsOnce( - document.body, - '.o_MessagingMenuContainer_spinner', - "messaging menu container should have spinner when messaging is not yet created" - ); + const messagingBeforeCreationDeferred = makeTestPromise(); + await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + assert.containsOnce( + document.body, + ".o_MessagingMenuContainer_spinner", + "messaging menu container should have spinner when messaging is not yet created" + ); - // simulate messaging becoming created - await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); - assert.containsOnce( - document.body, - '.o_MessagingMenu', - "messaging menu container should contain messaging menu after messaging has been created" - ); -}); - -QUnit.test('messaging not initialized', async function (assert) { - assert.expect(2); - - const messaginginitializedDeferred = makeTestPromise(); - const { click } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized + // simulate messaging becoming created + await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); + assert.containsOnce( + document.body, + ".o_MessagingMenu", + "messaging menu container should contain messaging menu after messaging has been created" + ); } - }, - waitUntilMessagingCondition: 'created', - }); - assert.strictEqual( - document.querySelectorAll('.o_MessagingMenu_loading').length, - 1, - "should display loading icon on messaging menu when messaging not yet initialized" - ); + ); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelector('.o_MessagingMenu_dropdownMenu').textContent, - "Please wait...", - "should prompt loading when opening messaging menu" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); + QUnit.test("messaging not initialized", async function (assert) { + assert.expect(2); -QUnit.test('messaging becomes initialized', async function (assert) { - assert.expect(2); + const messaginginitializedDeferred = makeTestPromise(); + const { click } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + waitUntilMessagingCondition: "created", + }); + assert.strictEqual( + document.querySelectorAll(".o_MessagingMenu_loading").length, + 1, + "should display loading icon on messaging menu when messaging not yet initialized" + ); - const messagingInitializedProm = makeTestPromise(); + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelector(".o_MessagingMenu_dropdownMenu").textContent, + "Please wait...", + "should prompt loading when opening messaging menu" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + }); - const { click } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messagingInitializedProm; - } - }, - waitUntilMessagingCondition: 'created', - }); - await click(`.o_MessagingMenu_toggler`); + QUnit.test("messaging becomes initialized", async function (assert) { + assert.expect(2); - // simulate messaging becomes initialized - await afterNextRender(() => messagingInitializedProm.resolve()); - assert.strictEqual( - document.querySelectorAll('.o_MessagingMenu_loading').length, - 0, - "should no longer display loading icon on messaging menu when messaging becomes initialized" - ); - assert.notOk( - document.querySelector('.o_MessagingMenu_dropdownMenu').textContent.includes("Please wait..."), - "should no longer prompt loading when opening messaging menu when messaging becomes initialized" - ); -}); + const messagingInitializedProm = makeTestPromise(); -QUnit.test('basic rendering', async function (assert) { - assert.expect(21); + const { click } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messagingInitializedProm; + } + }, + waitUntilMessagingCondition: "created", + }); + await click(`.o_MessagingMenu_toggler`); - patchWithCleanup(browser, { - Notification: { - ...browser.Notification, - permission: 'denied', - }, - }); - const { click } = await start(); - assert.strictEqual( - document.querySelectorAll('.o_MessagingMenu').length, - 1, - "should have messaging menu" - ); - assert.notOk( - document.querySelector('.o_MessagingMenu').classList.contains('show'), - "should not mark messaging menu item as shown by default" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_toggler`).length, - 1, - "should have clickable element on messaging menu" - ); - assert.notOk( - document.querySelector(`.o_MessagingMenu_toggler`).classList.contains('show'), - "should not mark messaging menu clickable item as shown by default" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_icon`).length, - 1, - "should have icon on clickable element in messaging menu" - ); - assert.ok( - document.querySelector(`.o_MessagingMenu_icon`).classList.contains('fa-comments'), - "should have 'comments' icon on clickable element in messaging menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "should not display any messaging menu dropdown by default" - ); + // simulate messaging becomes initialized + await afterNextRender(() => messagingInitializedProm.resolve()); + assert.strictEqual( + document.querySelectorAll(".o_MessagingMenu_loading").length, + 0, + "should no longer display loading icon on messaging menu when messaging becomes initialized" + ); + assert.notOk( + document + .querySelector(".o_MessagingMenu_dropdownMenu") + .textContent.includes("Please wait..."), + "should no longer prompt loading when opening messaging menu when messaging becomes initialized" + ); + }); - await click(`.o_MessagingMenu_toggler`); - assert.hasClass( - document.querySelector('.o_MessagingMenu'), - "show", - "should mark messaging menu as opened" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 1, - "should display messaging menu dropdown after click" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenuHeader`).length, - 1, - "should have dropdown menu header" - ); - assert.strictEqual( - document.querySelectorAll(` + QUnit.test("basic rendering", async function (assert) { + assert.expect(21); + + patchWithCleanup(browser, { + Notification: { + ...browser.Notification, + permission: "denied", + }, + }); + const { click } = await start(); + assert.strictEqual( + document.querySelectorAll(".o_MessagingMenu").length, + 1, + "should have messaging menu" + ); + assert.notOk( + document.querySelector(".o_MessagingMenu").classList.contains("show"), + "should not mark messaging menu item as shown by default" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_toggler`).length, + 1, + "should have clickable element on messaging menu" + ); + assert.notOk( + document.querySelector(`.o_MessagingMenu_toggler`).classList.contains("show"), + "should not mark messaging menu clickable item as shown by default" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_icon`).length, + 1, + "should have icon on clickable element in messaging menu" + ); + assert.ok( + document.querySelector(`.o_MessagingMenu_icon`).classList.contains("fa-comments"), + "should have 'comments' icon on clickable element in messaging menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "should not display any messaging menu dropdown by default" + ); + + await click(`.o_MessagingMenu_toggler`); + assert.hasClass( + document.querySelector(".o_MessagingMenu"), + "show", + "should mark messaging menu as opened" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 1, + "should display messaging menu dropdown after click" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenuHeader`).length, + 1, + "should have dropdown menu header" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenuHeader .o_MessagingMenuTabView `).length, - 3, - "should have 3 tab buttons to filter items in the header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, - 1, - "1 tab button should be 'All'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, - 1, - "1 tab button should be 'Chat'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, - 1, - "1 tab button should be 'Channels'" - ); - assert.ok( - document.querySelector(` + 3, + "should have 3 tab buttons to filter items in the header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, + 1, + "1 tab button should be 'All'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, + 1, + "1 tab button should be 'Chat'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, + 1, + "1 tab button should be 'Channels'" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should not be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should not be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should not be active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 1, - "should have button to make a new message" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "'channel' tab button should not be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 1, + "should have button to make a new message" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_NotificationListView `).length, - 1, - "should display thread preview list" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should display thread preview list" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_NotificationListView_noConversation `).length, - 1, - "should display no conversation in thread preview list" - ); + 1, + "should display no conversation in thread preview list" + ); - await click(`.o_MessagingMenu_toggler`); - assert.doesNotHaveClass( - document.querySelector('.o_MessagingMenu'), - "show", - "should mark messaging menu as closed" - ); -}); + await click(`.o_MessagingMenu_toggler`); + assert.doesNotHaveClass( + document.querySelector(".o_MessagingMenu"), + "show", + "should mark messaging menu as closed" + ); + }); -QUnit.test('counter is taking into account failure notification', async function (assert) { - assert.expect(2); + QUnit.test("counter is taking into account failure notification", async function (assert) { + assert.expect(2); - patchWithCleanup(browser, { - Notification: { - ...browser.Notification, - permission: 'denied', - }, - }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'mail.channel', - res_id: mailChannelId1, - }); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - // failure that is expected to be used in the test - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of the related message - notification_status: 'exception', // necessary value to have a failure - notification_type: 'email', - }); - await start(); + patchWithCleanup(browser, { + Notification: { + ...browser.Notification, + permission: "denied", + }, + }); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + }); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + // failure that is expected to be used in the test + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of the related message + notification_status: "exception", // necessary value to have a failure + notification_type: "email", + }); + await start(); - assert.containsOnce( - document.body, - '.o_MessagingMenu_counter', - "should display a notification counter next to the messaging menu for one notification" - ); - assert.strictEqual( - document.querySelector('.o_MessagingMenu_counter').textContent, - "1", - "should display a counter of '1' next to the messaging menu" - ); -}); + assert.containsOnce( + document.body, + ".o_MessagingMenu_counter", + "should display a notification counter next to the messaging menu for one notification" + ); + assert.strictEqual( + document.querySelector(".o_MessagingMenu_counter").textContent, + "1", + "should display a counter of '1' next to the messaging menu" + ); + }); -QUnit.test('switch tab', async function (assert) { - assert.expect(15); + QUnit.test("switch tab", async function (assert) { + assert.expect(15); - const { click } = await start(); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, - 1, - "1 tab button should be 'All'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, - 1, - "1 tab button should be 'Chat'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, - 1, - "1 tab button should be 'Channels'" - ); - assert.ok( - document.querySelector(` + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, + 1, + "1 tab button should be 'All'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, + 1, + "1 tab button should be 'Chat'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, + 1, + "1 tab button should be 'Channels'" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should not be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should not be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should not be active" - ); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should not be active" + ); - await click(`.o_MessagingMenuTabView[data-tab-id="chat"]`); - assert.notOk( - document.querySelector(` + await click(`.o_MessagingMenuTabView[data-tab-id="chat"]`); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should become inactive" - ); - assert.ok( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should become inactive" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should not become active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should not become active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should stay inactive" - ); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should stay inactive" + ); - await click(`.o_MessagingMenuTabView[data-tab-id="channel"]`); - assert.notOk( - document.querySelector(` + await click(`.o_MessagingMenuTabView[data-tab-id="channel"]`); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should stay active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should stay active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should become inactive" - ); - assert.ok( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should become inactive" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should become active" - ); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should become active" + ); - await click(`.o_MessagingMenuTabView[data-tab-id="all"]`); - assert.ok( - document.querySelector(` + await click(`.o_MessagingMenuTabView[data-tab-id="all"]`); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should become active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should become active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should stay inactive" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should stay inactive" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should become inactive" - ); -}); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should become inactive" + ); + }); -QUnit.test('new message', async function (assert) { - assert.expect(3); + QUnit.test("new message", async function (assert) { + assert.expect(3); - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - assert.ok( - document.querySelector(`.o_ChatWindow`).classList.contains('o-new-message'), - "chat window should be for new message" - ); - assert.ok( - document.querySelector(`.o_ChatWindow`).classList.contains('o-focused'), - "chat window should be focused" - ); -}); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + assert.ok( + document.querySelector(`.o_ChatWindow`).classList.contains("o-new-message"), + "chat window should be for new message" + ); + assert.ok( + document.querySelector(`.o_ChatWindow`).classList.contains("o-focused"), + "chat window should be focused" + ); + }); -QUnit.test('no new message when discuss is open', async function (assert) { - assert.expect(3); + QUnit.test("no new message when discuss is open", async function (assert) { + assert.expect(3); - const { click, openDiscuss, openView } = await start(); - await openDiscuss({ waitUntilMessagesLoaded: false }); + const { click, openDiscuss, openView } = await start(); + await openDiscuss({ waitUntilMessagesLoaded: false }); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 0, - "should not have 'new message' when discuss is open" - ); + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 0, + "should not have 'new message' when discuss is open" + ); - await openView({ - res_model: 'res.partner', - views: [[false, 'form']], - }); + await openView({ + res_model: "res.partner", + views: [[false, "form"]], + }); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 1, - "should have 'new message' when discuss is closed" - ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 1, + "should have 'new message' when discuss is closed" + ); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 0, - "should not have 'new message' when discuss is open again" - ); -}); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 0, + "should not have 'new message' when discuss is open again" + ); + }); -QUnit.test('channel preview: basic rendering', async function (assert) { - assert.expect(9); + QUnit.test("channel preview: basic rendering", async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - pyEnv['mail.message'].create({ - author_id: resPartnerId1, // not current partner, will be asserted in the test - body: "test
", // random body, will be asserted in the test - model: 'mail.channel', // necessary to link message to channel - res_id: mailChannelId1, // id of related channel - }); - const { click } = await start(); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + pyEnv["mail.message"].create({ + author_id: resPartnerId1, // not current partner, will be asserted in the test + body: "test
", // random body, will be asserted in the test + model: "mail.channel", // necessary to link message to channel + res_id: mailChannelId1, // id of related channel + }); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(` + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView `).length, - 1, - "should have one preview" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have one preview" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_sidebar `).length, - 1, - "preview should have a sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have a sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_content `).length, - 1, - "preview should have some content" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have some content" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_header `).length, - 1, - "preview should have header in content" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have header in content" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_header .o_ChannelPreviewView_name `).length, - 1, - "preview should have name in header of content" - ); - assert.strictEqual( - document.querySelector(` + 1, + "preview should have name in header of content" + ); + assert.strictEqual( + document.querySelector(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_name `).textContent, - "General", "preview should have name of channel" - ); - assert.strictEqual( - document.querySelectorAll(` + "General", + "preview should have name of channel" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_content .o_ChannelPreviewView_core `).length, - 1, - "preview should have core in content" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have core in content" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_core .o_ChannelPreviewView_inlineText `).length, - 1, - "preview should have inline text in core of content" - ); - assert.strictEqual( - document.querySelector(` + 1, + "preview should have inline text in core of content" + ); + assert.strictEqual( + document + .querySelector( + ` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_core .o_ChannelPreviewView_inlineText - `).textContent.trim(), - "Demo: test", - "preview should have message content as inline text of core content" - ); -}); + ` + ) + .textContent.trim(), + "Demo: test", + "preview should have message content as inline text of core content" + ); + }); -QUnit.test('filtered previews', async function (assert) { - assert.expect(12); + QUnit.test("filtered previews", async function (assert) { + assert.expect(12); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { channel_type: "chat" }, - { name: "mailChannel1" }, - ]); - pyEnv['mail.message'].create([ - { - model: 'mail.channel', // to link message to channel - res_id: mailChannelId1, // id of related channel - }, - { - model: 'mail.channel', // to link message to channel - res_id: mailChannelId2, // id of related channel - }, - ]); - const { click } = await start(); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { channel_type: "chat" }, + { name: "mailChannel1" }, + ]); + pyEnv["mail.message"].create([ + { + model: "mail.channel", // to link message to channel + res_id: mailChannelId1, // id of related channel + }, + { + model: "mail.channel", // to link message to channel + res_id: mailChannelId2, // id of related channel + }, + ]); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length, - 2, - "should have 2 previews" - ); - assert.strictEqual( - document.querySelectorAll(` + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`) + .length, + 2, + "should have 2 previews" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have preview of channel" - ); + 1, + "should have preview of channel" + ); - await click('.o_MessagingMenuTabView[data-tab-id="chat"]'); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length, - 1, - "should have one preview" - ); - assert.strictEqual( - document.querySelectorAll(` + await click('.o_MessagingMenuTabView[data-tab-id="chat"]'); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`) + .length, + 1, + "should have one preview" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 0, - "should not have preview of channel" - ); + 0, + "should not have preview of channel" + ); - await click('.o_MessagingMenuTabView[data-tab-id="channel"]'); - assert.strictEqual( - document.querySelectorAll(` + await click('.o_MessagingMenuTabView[data-tab-id="channel"]'); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView `).length, - 1, - "should have one preview" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have one preview" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 0, - "should not have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 0, + "should not have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have preview of channel" - ); + 1, + "should have preview of channel" + ); - await click('.o_MessagingMenuTabView[data-tab-id="all"]'); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length, - 2, - "should have 2 previews" - ); - assert.strictEqual( - document.querySelectorAll(` + await click('.o_MessagingMenuTabView[data-tab-id="all"]'); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`) + .length, + 2, + "should have 2 previews" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have preview of channel" - ); -}); + 1, + "should have preview of channel" + ); + }); -QUnit.test('open chat window from preview', async function (assert) { - assert.expect(1); + QUnit.test("open chat window from preview", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - const { click } = await start(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); -}); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + }); -QUnit.test('no code injection in message body preview', async function (assert) { - assert.expect(5); + QUnit.test("no code injection in message body preview", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: "&shoulnotberaised
", - model: "mail.channel", - res_id: mailChannelId1, - }); - const { click } = await start(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: + "&shoulnotberaised
", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.containsOnce( - document.body, - '.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView', - "should display a preview", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_core', - "preview should have core in content", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_inlineText', - "preview should have inline text in core of content", - ); - assert.strictEqual( - document.querySelector('.o_ChannelPreviewView_inlineText') - .textContent.replace(/\s/g, ""), - "You:&shoulnotberaisedthrownewError('CodeInjectionError');", - "should display correct uninjected last message inline content" - ); - assert.containsNone( - document.querySelector('.o_ChannelPreviewView_inlineText'), - 'script', - "last message inline content should not have any code injection" - ); -}); + await click(`.o_MessagingMenu_toggler`); + assert.containsOnce( + document.body, + ".o_MessagingMenu_dropdownMenu .o_ChannelPreviewView", + "should display a preview" + ); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_core", + "preview should have core in content" + ); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_inlineText", + "preview should have inline text in core of content" + ); + assert.strictEqual( + document + .querySelector(".o_ChannelPreviewView_inlineText") + .textContent.replace(/\s/g, ""), + "You:&shoulnotberaisedthrownewError('CodeInjectionError');", + "should display correct uninjected last message inline content" + ); + assert.containsNone( + document.querySelector(".o_ChannelPreviewView_inlineText"), + "script", + "last message inline content should not have any code injection" + ); + }); -QUnit.test('no code injection in message body preview from sanitized message', async function (assert) { - assert.expect(5); + QUnit.test( + "no code injection in message body preview from sanitized message", + async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: "<em>&shoulnotberaised</em><script>throw new Error('CodeInjectionError');</script>
", - model: "mail.channel", - res_id: mailChannelId1, - }); - const { click } = await start(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: + "<em>&shoulnotberaised</em><script>throw new Error('CodeInjectionError');</script>
", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.containsOnce( - document.body, - '.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView', - "should display a preview", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_core', - "preview should have core in content", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_inlineText', - "preview should have inline text in core of content", - ); - assert.strictEqual( - document.querySelector('.o_ChannelPreviewView_inlineText') - .textContent.replace(/\s/g, ""), - "You:&shoulnotberaised", - "should display correct uninjected last message inline content" - ); - assert.containsNone( - document.querySelector('.o_ChannelPreviewView_inlineText'), - 'script', - "last message inline content should not have any code injection" - ); -}); - -QUnit.test('a
b
c
d
a
b
c
d
New message !
", - date: "2020-03-23 10:00:00", - id: 44, - message_type: 'comment', - model: 'mail.channel', - record_name: 'Channel 2019', - res_id: mailChannelId1, - }, - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + author_id: [7, "Demo User"], + body: "New message !
", + date: "2020-03-23 10:00:00", + id: 44, + message_type: "comment", + model: "mail.channel", + record_name: "Channel 2019", + res_id: mailChannelId1, + }, + }); + }); + assert.containsN( + document.body, + ".o_ChannelPreviewView", + 2, + "there should still be two thread previews" + ); + const channelPreviewViewElList = document.querySelectorAll(".o_ChannelPreviewView"); + assert.strictEqual( + channelPreviewViewElList[0].querySelector(":scope .o_ChannelPreviewView_name") + .textContent, + "Channel 2019", + "First channel in the list should now be 'Channel 2019'" + ); + assert.strictEqual( + channelPreviewViewElList[1].querySelector(":scope .o_ChannelPreviewView_name") + .textContent, + "Channel 2020", + "Second channel in the list should now be 'Channel 2020'" + ); + } + ); }); - assert.containsN( - document.body, - '.o_ChannelPreviewView', - 2, - "there should still be two thread previews" - ); - const channelPreviewViewElList = document.querySelectorAll('.o_ChannelPreviewView'); - assert.strictEqual( - channelPreviewViewElList[0].querySelector(':scope .o_ChannelPreviewView_name').textContent, - 'Channel 2019', - "First channel in the list should now be 'Channel 2019'" - ); - assert.strictEqual( - channelPreviewViewElList[1].querySelector(':scope .o_ChannelPreviewView_name').textContent, - 'Channel 2020', - "Second channel in the list should now be 'Channel 2020'" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js index 3aca4877a48..9e172ed4982 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js @@ -1,208 +1,210 @@ /** @odoo-module **/ -import { UPDATE_BUS_PRESENCE_DELAY } from '@bus/im_status_service'; +import { UPDATE_BUS_PRESENCE_DELAY } from "@bus/im_status_service"; -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('persona_im_status_icon_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("persona_im_status_icon_tests.js"); -QUnit.test('initially online', async function (assert) { - assert.expect(1); + QUnit.test("initially online", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'online' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "online" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering" + ); + }); + + QUnit.test("initially offline", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "offline" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, + 1, + "persona IM status icon should have offline status rendering" + ); + }); + + QUnit.test("initially away", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "away" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, + 1, + "persona IM status icon should have away status rendering" + ); + }); + + QUnit.test("change icon on change partner im_status", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "online" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering" + ); + + pyEnv["res.partner"].write([partnerId], { im_status: "offline" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, + 1, + "persona IM status icon should have offline status rendering" + ); + + pyEnv["res.partner"].write([partnerId], { im_status: "away" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, + 1, + "persona IM status icon should have away status rendering" + ); + + pyEnv["res.partner"].write([partnerId], { im_status: "online" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering in the end" + ); + }); + + QUnit.test("change icon on change guest im_status", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ im_status: "online" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { guest_id: guestId }], + ], + channel_type: "group", + }); + pyEnv["mail.message"].create({ + author_guest_id: guestId, + author_id: false, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering" + ); + + pyEnv["mail.guest"].write([guestId], { im_status: "offline" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, + 1, + "persona IM status icon should have offline status rendering" + ); + + pyEnv["mail.guest"].write([guestId], { im_status: "away" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, + 1, + "persona IM status icon should have away status rendering" + ); + + pyEnv["mail.guest"].write([guestId], { im_status: "online" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering in the end" + ); + }); }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering" - ); -}); - -QUnit.test('initially offline', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'offline' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, - 1, - "persona IM status icon should have offline status rendering" - ); -}); - -QUnit.test('initially away', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'away' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, - 1, - "persona IM status icon should have away status rendering" - ); -}); - -QUnit.test('change icon on change partner im_status', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'online' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering" - ); - - pyEnv['res.partner'].write([partnerId], { im_status: 'offline' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, - 1, - "persona IM status icon should have offline status rendering" - ); - - pyEnv['res.partner'].write([partnerId], { im_status: 'away' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, - 1, - "persona IM status icon should have away status rendering" - ); - - pyEnv['res.partner'].write([partnerId], { im_status: 'online' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering in the end" - ); -}); - -QUnit.test('change icon on change guest im_status', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const guestId = pyEnv['mail.guest'].create({ im_status: 'online' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [[0, 0, { partner_id: pyEnv.currentPartnerId }], [0, 0, { guest_id: guestId }]], - channel_type: 'group', - }); - pyEnv['mail.message'].create({ - author_guest_id: guestId, - author_id: false, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering" - ); - - pyEnv['mail.guest'].write([guestId], { im_status: 'offline' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, - 1, - "persona IM status icon should have offline status rendering" - ); - - pyEnv['mail.guest'].write([guestId], { im_status: 'away' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, - 1, - "persona IM status icon should have away status rendering" - ); - - pyEnv['mail.guest'].write([guestId], { im_status: 'online' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering in the end" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js index 79ea8701158..a2d338a2740 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js @@ -1,83 +1,82 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_icon_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_icon_tests.js"); -QUnit.test('chat: correspondent is typing', async function (assert) { - assert.expect(5); + QUnit.test("chat: correspondent is typing", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - im_status: 'online', - name: 'Demo', + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + im_status: "online", + name: "Demo", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body.querySelector(".o_DiscussSidebarCategoryItem"), + ".o_ThreadIconView", + "should have thread icon in the sidebar" + ); + assert.containsOnce( + document.body, + ".o_ThreadIconView_online", + "should have thread icon with persona IM status icon 'online'" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadIconView_typing", + "should have thread icon with partner currently typing" + ); + assert.strictEqual( + document.querySelector(".o_ThreadIconView_typing").title, + "Demo is typing...", + "title of icon should tell demo is currently typing" + ); + + // simulate receive typing notification from demo "no longer is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: false, + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadIconView_online", + "should have thread icon with persona IM status icon 'online' (no longer typing)" + ); + }); }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body.querySelector('.o_DiscussSidebarCategoryItem'), - '.o_ThreadIconView', - "should have thread icon in the sidebar" - ); - assert.containsOnce( - document.body, - '.o_ThreadIconView_online', - "should have thread icon with persona IM status icon 'online'" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadIconView_typing', - "should have thread icon with partner currently typing" - ); - assert.strictEqual( - document.querySelector('.o_ThreadIconView_typing').title, - "Demo is typing...", - "title of icon should tell demo is currently typing" - ); - - // simulate receive typing notification from demo "no longer is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': false, - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadIconView_online', - "should have thread icon with persona IM status icon 'online' (no longer typing)" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js index 20af52e0085..c11145ce8cd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js @@ -1,337 +1,361 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_needaction_preview_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_needaction_preview_tests.js"); -QUnit.test('mark as read', async function (assert) { - assert.expect(5); + QUnit.test("mark as read", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start({ - async mockRPC(route, args) { - if (route.includes('mark_all_as_read')) { - assert.step('mark_all_as_read'); - assert.deepEqual( - args.kwargs.domain, - [ - ['model', '=', 'res.partner'], - ['res_id', '=', resPartnerId1], - ], - "should mark all as read the correct thread" + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start({ + async mockRPC(route, args) { + if (route.includes("mark_all_as_read")) { + assert.step("mark_all_as_read"); + assert.deepEqual( + args.kwargs.domain, + [ + ["model", "=", "res.partner"], + ["res_id", "=", resPartnerId1], + ], + "should mark all as read the correct thread" + ); + } + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView_markAsRead", + "should have 1 mark as read button" + ); + + await click(".o_ThreadNeedactionPreviewView_markAsRead"); + assert.verifySteps(["mark_all_as_read"], "should have marked the thread as read"); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have opened the thread" + ); + }); + + QUnit.test( + "click on preview should mark as read and open the thread", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start(); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have a preview initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have no chat window initially" + ); + + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the thread on clicking on the preview" + ); + await click(".o_MessagingMenu_toggler"); + assert.containsNone( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have no preview because the message should be marked as read after opening its thread" ); } - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView_markAsRead', - "should have 1 mark as read button" - ); + ); - await click('.o_ThreadNeedactionPreviewView_markAsRead'); - assert.verifySteps( - ['mark_all_as_read'], - "should have marked the thread as read" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have opened the thread" - ); -}); + QUnit.test( + "click on expand from chat window should close the chat window and open the form view", + async function (assert) { + assert.expect(8); -QUnit.test('click on preview should mark as read and open the thread', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("do_action"); + assert.strictEqual( + action.res_id, + resPartnerId1, + "should redirect to the id of the thread" + ); + assert.strictEqual( + action.res_model, + "res.partner", + "should redirect to the model of the thread" + ); + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have a preview initially" + ); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start(); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have a preview initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have no chat window initially" - ); + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the thread on clicking on the preview" + ); + assert.containsOnce( + document.body, + ".o_ChatWindowHeaderView_commandExpand", + "should have an expand button" + ); - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the thread on clicking on the preview" - ); - await click('.o_MessagingMenu_toggler'); - assert.containsNone( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have no preview because the message should be marked as read after opening its thread" - ); -}); - -QUnit.test('click on expand from chat window should close the chat window and open the form view', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, env, messaging } = await start(); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('do_action'); - assert.strictEqual( - action.res_id, - resPartnerId1, - "should redirect to the id of the thread" - ); - assert.strictEqual( - action.res_model, - 'res.partner', - "should redirect to the model of the thread" - ); - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have a preview initially" - ); - - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the thread on clicking on the preview" - ); - assert.containsOnce( - document.body, - '.o_ChatWindowHeaderView_commandExpand', - "should have an expand button" - ); - - await click('.o_ChatWindowHeaderView_commandExpand'); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have closed the chat window on clicking expand" - ); - assert.verifySteps( - ['do_action'], - "should have done an action to open the form view" - ); -}); - -QUnit.test('[technical] opening a non-channel chat window should not call channel_fold', async function (assert) { - // channel_fold should not be called when opening non-channels in chat - // window, because there is no server sync of fold state for them. - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start({ - async mockRPC(route, args) { - if (route.includes('channel_fold')) { - const message = "should not call channel_fold when opening a non-channel chat window"; - assert.step(message); - console.error(message); - throw Error(message); + await click(".o_ChatWindowHeaderView_commandExpand"); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have closed the chat window on clicking expand" + ); + assert.verifySteps( + ["do_action"], + "should have done an action to open the form view" + ); } - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have a preview initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have no chat window initially" - ); + ); - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the chat window on clicking on the preview" - ); -}); - -QUnit.test('preview should display last needaction message preview even if there is a more recent message that is not needaction in the thread', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Stranger", - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "I am the oldest but needaction", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "I am more recent", - model: 'res.partner', - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging } = await start(); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView_inlineText', - "should have a preview from the last message" - ); - assert.strictEqual( - document.querySelector('.o_ThreadNeedactionPreviewView_inlineText').textContent, - 'Stranger: I am the oldest but needaction', - "the displayed message should be the one that needs action even if there is a more recent message that is not needaction on the thread" - ); -}); - -QUnit.test('chat window header should not have unread counter for non-channel thread', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: 'not empty', - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start(); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the chat window on clicking on the preview" - ); - assert.containsNone( - document.body, - '.o_ChatWindowHeaderView_counter', - "chat window header should not have unread counter for non-channel thread" - ); -}); - -}); + QUnit.test( + "[technical] opening a non-channel chat window should not call channel_fold", + async function (assert) { + // channel_fold should not be called when opening non-channels in chat + // window, because there is no server sync of fold state for them. + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start({ + async mockRPC(route, args) { + if (route.includes("channel_fold")) { + const message = + "should not call channel_fold when opening a non-channel chat window"; + assert.step(message); + console.error(message); + throw Error(message); + } + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have a preview initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have no chat window initially" + ); + + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the chat window on clicking on the preview" + ); + } + ); + + QUnit.test( + "preview should display last needaction message preview even if there is a more recent message that is not needaction in the thread", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Stranger", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "I am the oldest but needaction", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "I am more recent", + model: "res.partner", + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging } = await start(); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView_inlineText", + "should have a preview from the last message" + ); + assert.strictEqual( + document.querySelector(".o_ThreadNeedactionPreviewView_inlineText").textContent, + "Stranger: I am the oldest but needaction", + "the displayed message should be the one that needs action even if there is a more recent message that is not needaction on the thread" + ); + } + ); + + QUnit.test( + "chat window header should not have unread counter for non-channel thread", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start(); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the chat window on clicking on the preview" + ); + assert.containsNone( + document.body, + ".o_ChatWindowHeaderView_counter", + "chat window header should not have unread counter for non-channel thread" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js index 553f43b65f0..876013abdc2 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js @@ -5,348 +5,379 @@ import { nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_textual_typing_status_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_textual_typing_status_tests.js"); -QUnit.test('receive other member typing status "is typing"', async function (assert) { - assert.expect(2); + QUnit.test('receive other member typing status "is typing"', async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + }); + + QUnit.test( + 'receive other member typing status "is typing" then "no longer is typing"', + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + + // simulate receive typing notification from demo "is no longer typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: false, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should no longer display that demo user is typing" + ); + } + ); + + QUnit.test( + 'assume other member typing status becomes "no longer is typing" after 60 seconds without any updated typing status', + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { advanceTime, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + + await afterNextRender(() => advanceTime(60 * 1000)); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should no longer display that demo user is typing" + ); + } + ); + + QUnit.test( + 'other member typing status "is typing" refreshes 60 seconds timer of assuming no longer typing', + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { advanceTime, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + + // simulate receive typing notification from demo "is typing" again after 50s. + await advanceTime(50 * 1000); + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }); + await advanceTime(50 * 1000); + await nextAnimationFrame(); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should still display that demo user is typing after 100 seconds (refreshed is typing status at 50s => (100 - 50) = 50s < 60s after assuming no-longer typing)" + ); + + await afterNextRender(() => advanceTime(11 * 1000)); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should no longer display that demo user is typing after 111 seconds (refreshed is typing status at 50s => (111 - 50) = 61s > 60s after assuming no-longer typing)" + ); + } + ); + + QUnit.test( + 'receive several other members typing status "is typing"', + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { name: "Other 10" }, + { name: "Other 11" }, + { name: "Other 12" }, + ]); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + [0, 0, { partner_id: resPartnerId3 }], + ], + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from other 10 (is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 10 is typing...", + "Should display that 'Other 10' member is typing" + ); + + // simulate receive typing notification from other 11 (is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId2, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 10 and Other 11 are typing...", + "Should display that members 'Other 10' and 'Other 11' are typing (order: longer typer named first)" + ); + + // simulate receive typing notification from other 12 (is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId3, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 10, Other 11 and more are typing...", + "Should display that members 'Other 10', 'Other 11' and more (at least 1 extra member) are typing (order: longer typer named first)" + ); + + // simulate receive typing notification from other 10 (no longer is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: false, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 11 and Other 12 are typing...", + "Should display that members 'Other 11' and 'Other 12' are typing ('Other 10' stopped typing)" + ); + + // simulate receive typing notification from other 10 (is typing again) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 11, Other 12 and more are typing...", + "Should display that members 'Other 11' and 'Other 12' and more (at least 1 extra member) are typing (order by longer typer, 'Other 10' just recently restarted typing)" + ); + } + ); }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); -}); - -QUnit.test('receive other member typing status "is typing" then "no longer is typing"', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); - - // simulate receive typing notification from demo "is no longer typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': false, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should no longer display that demo user is typing" - ); -}); - -QUnit.test('assume other member typing status becomes "no longer is typing" after 60 seconds without any updated typing status', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { advanceTime, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); - - await afterNextRender(() => advanceTime(60 * 1000)); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should no longer display that demo user is typing" - ); -}); - -QUnit.test ('other member typing status "is typing" refreshes 60 seconds timer of assuming no longer typing', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { advanceTime, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); - - // simulate receive typing notification from demo "is typing" again after 50s. - await advanceTime(50 * 1000); - messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - }); - await advanceTime(50 * 1000); - await nextAnimationFrame(); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should still display that demo user is typing after 100 seconds (refreshed is typing status at 50s => (100 - 50) = 50s < 60s after assuming no-longer typing)" - ); - - await afterNextRender(() => advanceTime(11 * 1000)); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should no longer display that demo user is typing after 111 seconds (refreshed is typing status at 50s => (111 - 50) = 61s > 60s after assuming no-longer typing)" - ); -}); - -QUnit.test('receive several other members typing status "is typing"', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { name: 'Other 10' }, - { name: 'Other 11' }, - { name: 'Other 12' }, - ]); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - [0, 0, { partner_id: resPartnerId3 }], - ], - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from other 10 (is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 10 is typing...", - "Should display that 'Other 10' member is typing" - ); - - // simulate receive typing notification from other 11 (is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId2, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 10 and Other 11 are typing...", - "Should display that members 'Other 10' and 'Other 11' are typing (order: longer typer named first)" - ); - - // simulate receive typing notification from other 12 (is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId3, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 10, Other 11 and more are typing...", - "Should display that members 'Other 10', 'Other 11' and more (at least 1 extra member) are typing (order: longer typer named first)" - ); - - // simulate receive typing notification from other 10 (no longer is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': false, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 11 and Other 12 are typing...", - "Should display that members 'Other 11' and 'Other 12' are typing ('Other 10' stopped typing)" - ); - - // simulate receive typing notification from other 10 (is typing again) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 11, Other 12 and more are typing...", - "Should display that members 'Other 11' and 'Other 12' and more (at least 1 extra member) are typing (order by longer typer, 'Other 10' just recently restarted typing)" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js index 2bd044e8b4c..9b92c9317bc 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js @@ -6,1636 +6,1792 @@ import { isScrolledToBottom, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_view_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_view_tests.js"); -QUnit.test('dragover files on thread with composer', async function (assert) { - assert.expect(1); + QUnit.test("dragover files on thread with composer", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "General", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await afterNextRender(() => - dragenterFiles(document.querySelector('.o_ThreadView')) - ); - assert.ok( - document.querySelector('.o_ComposerView_dropZone'), - "should have dropzone when dragging file over the thread" - ); -}); - -QUnit.test('message list asc order', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "General", - }); - for (let i = 0; i <= 60; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until channel 1 loaded initial messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - const messageItems = document.querySelectorAll(`.o_MessageListView_item`); - assert.notOk( - messageItems[messageItems.length - 1].classList.contains("o_MessageListView_loadMore"), - "load more link should be before messages" - ); - assert.ok( - messageItems[0].classList.contains("o_MessageListView_loadMore"), - "load more link should NOT be after messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 30, - "should have 30 messages at the beginning" - ); - - // scroll to top - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 0, - message: "should wait until channel 1 loaded more messages after scrolling to top", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 60, - "should have 60 messages after scrolled to top" - ); - - // scroll to bottom - await afterNextRender(() => { - document.querySelector(`.o_ThreadView_messageList`).scrollTop = - document.querySelector(`.o_ThreadView_messageList`).scrollHeight; - }); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 60, - "scrolling to bottom should not trigger any message fetching" - ); -}); - -QUnit.test('mark channel as fetched when a new message is loaded and as seen when focusing composer [REQUIRE FOCUS]', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "fred@example.com", - name: "Fred", - }); - const resUsersId1 = pyEnv['res.users'].create({ - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { afterEvent, click, messaging } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fetched') { - assert.strictEqual( - args.args[0][0], - mailChannelId1, - 'channel_fetched is called on the right channel id' - ); - assert.strictEqual( - args.model, - 'mail.channel', - 'channel_fetched is called on the right channel model' - ); - assert.step('rpc:channel_fetch'); - } else if (route === '/mail/channel/set_last_seen_message') { - assert.strictEqual( - args.channel_id, - mailChannelId1, - 'set_last_seen_message is called on the right channel id' - ); - assert.step('rpc:set_last_seen_message'); - } - } - }); - await click(`.o_MessagingMenu_toggler`); - const thread = messaging.models['Thread'].findFromIdentifyingData({ - id: mailChannelId1, - model: 'mail.channel', - }); - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "new message", - uuid: thread.uuid, - }, - })); - assert.verifySteps( - ['rpc:channel_fetch'], - "Channel should have been fetched but not seen yet" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => document.querySelector('.o_ComposerTextInputView_textarea').focus(), - message: "should wait until last seen by current partner message id changed after focusing the thread", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' - ); - }, - })); - assert.verifySteps( - ['rpc:set_last_seen_message'], - "Channel should have been marked as seen after threadView got the focus" - ); -}); - -QUnit.test('mark channel as fetched and seen when a new message is loaded if composer is focused [REQUIRE FOCUS]', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const resUsersId1 = pyEnv['res.users'].create({ - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - mockRPC(route, args) { - if (args.method === 'channel_fetched' && args.args[0] === mailChannelId1) { - throw new Error("'channel_fetched' RPC must not be called for created channel as message is directly seen"); - } else if (route === '/mail/channel/set_last_seen_message') { - assert.strictEqual( - args.channel_id, - mailChannelId1, - 'set_last_seen_message is called on the right channel id' - ); - assert.step('rpc:set_last_seen_message'); - } - } - }); - await openDiscuss(); - document.querySelector('.o_ComposerTextInputView_textarea').focus(); - // simulate receiving a message - await afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, }, - message_content: "fdsfsd
", - uuid: messaging.models['Thread'].findFromIdentifyingData({ - model: 'mail.channel', - id: mailChannelId1, - }).uuid, - }, - }), - message: "should wait until last seen by current partner message id changed after receiving a message while thread is focused", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' + }); + await openDiscuss(); + await afterNextRender(() => dragenterFiles(document.querySelector(".o_ThreadView"))); + assert.ok( + document.querySelector(".o_ComposerView_dropZone"), + "should have dropzone when dragging file over the thread" ); - }, - }); - assert.verifySteps( - ['rpc:set_last_seen_message'], - "Channel should have been mark as seen directly" - ); -}); - -QUnit.test('show message subject when subject is not the same as the thread name', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "General", - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - subject: "Salutations, voyageur", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('do not show message subject when subject is the same as the thread name', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "Salutations, voyageur", - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - subject: "Salutations, voyageur", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject of the message" - ); -}); - -QUnit.test('[technical] new messages separator on posting message', async function (assert) { - // technical as we need to remove focus from text input to avoid `set_last_seen_message` call - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'channel', - name: "General", - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "first message", - model: "mail.channel", - res_id: mailChannelId1, - }); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display one message in thread initially" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - - await insertText('.o_ComposerTextInputView_textarea', "hey !"); - await afterNextRender(() => { - // need to remove focus from text area to avoid set_last_seen_message - document.querySelector('.o_ComposerView_buttonSend').focus(); - document.querySelector('.o_ComposerView_buttonSend').click(); - - }); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "should display 2 messages (initial & newly posted), after posting a message" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "still no separator shown when current partner posted a message" - ); -}); - -QUnit.test('new messages separator on receiving new message [REQUIRE FOCUS]', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'channel', - name: "General", - uuid: 'randomuuid', - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "blah", - model: "mail.channel", - res_id: mailChannelId1, - }); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageListView_message', - "should have an initial message" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - - document.querySelector('.o_ComposerTextInputView_textarea').blur(); - // simulate receiving a message - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: messaging.models['Thread'].findFromIdentifyingData({ - model: 'mail.channel', - id: mailChannelId1, - }).uuid, - }, - }), - message: "should wait until new message is received", - predicate: ({ hint, threadViewer }) => { - return ( - threadViewer.thread.id === mailChannelId1 && - threadViewer.thread.model === 'mail.channel' && - hint.type === 'message-received' - ); - }, - }); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "should now have 2 messages after receiving a new message" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "'new messages' separator should be shown" - ); - - assert.containsOnce( - document.body, - `.o_MessageListView_separatorNewMessages ~ .o_MessageView[data-message-id="${mailMessageId1 + 1}"]`, - "'new messages' separator should be shown above new message received" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => document.querySelector('.o_ComposerTextInputView_textarea').focus(), - message: "should wait until last seen by current partner message id changed after focusing the thread", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' - ); - }, - })); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "'new messages' separator should no longer be shown as last message has been seen" - ); -}); - -QUnit.test('new messages separator on posting message', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'channel', - name: "General", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_MessageListView_message', - "should have no messages" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - - await insertText('.o_ComposerTextInputView_textarea', "hey !"); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.body, - '.o_MessageView', - "should have the message current partner just posted" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "still no separator shown when current partner posted a message" - ); -}); - -QUnit.test('basic rendering of canceled notification', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Someone" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - message_type: 'email', - model: 'mail.channel', - res_id: mailChannelId1, - }); - pyEnv['mail.notification'].create({ - failure_type: 'SMTP', - mail_message_id: mailMessageId1, - notification_status: 'canceled', - notification_type: 'email', - res_partner_id: resPartnerId1, - }); - const { afterEvent, click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - - assert.containsOnce( - document.body, - '.o_MessageView_notificationIconClickable', - "should display the notification icon container on the message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIcon', - "should display the notification icon on the message" - ); - assert.hasClass( - document.querySelector('.o_MessageView_notificationIcon'), - 'fa-envelope-o', - "notification icon shown on the message should represent email" - ); - - await click('.o_MessageView_notificationIconClickable'); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView', - "notification popover should be opened after notification has been clicked" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationIcon', - "an icon should be shown in notification popover" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationIcon.fa.fa-trash-o', - "the icon shown in notification popover should be the canceled icon" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationPartnerName', - "partner name should be shown in notification popover" - ); - assert.strictEqual( - document.querySelector('.o_MessageNotificationPopoverContentView_notificationPartnerName').textContent.trim(), - "Someone", - "partner name shown in notification popover should be the one concerned by the notification" - ); -}); - -QUnit.test('should scroll to bottom on receiving new message if the list is initially scrolled to bottom (asc order)', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - // Needed partner & user to allow simulation of message reception - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i <= 10; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, }); - } - const { afterEvent, click, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await click(`.o_MessagingMenu_toggler`); - const thread = messaging.models['Thread'].findFromIdentifyingData({ model: 'mail.channel', id: mailChannelId1 }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - async func() { - await click(`.o_NotificationListView_preview`); - }, - message: "should wait until channel scrolled initially", - predicate: data => thread === data.threadViewer.thread, - }); - const initialMessageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(initialMessageList), - "should have scrolled to bottom of channel 20 initially" - ); - // simulate receiving a message - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => - messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, + QUnit.test("message list asc order", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "General", + }); + for (let i = 0; i <= 60; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until channel 1 loaded initial messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + const messageItems = document.querySelectorAll(`.o_MessageListView_item`); + assert.notOk( + messageItems[messageItems.length - 1].classList.contains( + "o_MessageListView_loadMore" + ), + "load more link should be before messages" + ); + assert.ok( + messageItems[0].classList.contains("o_MessageListView_loadMore"), + "load more link should NOT be after messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 30, + "should have 30 messages at the beginning" + ); + + // scroll to top + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => (document.querySelector(`.o_ThreadView_messageList`).scrollTop = 0), + message: "should wait until channel 1 loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 60, + "should have 60 messages after scrolled to top" + ); + + // scroll to bottom + await afterNextRender(() => { + document.querySelector( + `.o_ThreadView_messageList` + ).scrollTop = document.querySelector(`.o_ThreadView_messageList`).scrollHeight; + }); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 60, + "scrolling to bottom should not trigger any message fetching" + ); + }); + + QUnit.test( + "mark channel as fetched when a new message is loaded and as seen when focusing composer [REQUIRE FOCUS]", + async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "fred@example.com", + name: "Fred", + }); + const resUsersId1 = pyEnv["res.users"].create({ + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { afterEvent, click, messaging } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fetched") { + assert.strictEqual( + args.args[0][0], + mailChannelId1, + "channel_fetched is called on the right channel id" + ); + assert.strictEqual( + args.model, + "mail.channel", + "channel_fetched is called on the right channel model" + ); + assert.step("rpc:channel_fetch"); + } else if (route === "/mail/channel/set_last_seen_message") { + assert.strictEqual( + args.channel_id, + mailChannelId1, + "set_last_seen_message is called on the right channel id" + ); + assert.step("rpc:set_last_seen_message"); + } }, - message_content: "hello", - uuid: thread.uuid, - }, - }), - message: "should wait until channel scrolled after receiving a message", - predicate: data => thread === data.threadViewer.thread, - }); - const messageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(messageList), - "should scroll to bottom on receiving new message because the list is initially scrolled to bottom" - ); -}); + }); + await click(`.o_MessagingMenu_toggler`); + const thread = messaging.models["Thread"].findFromIdentifyingData({ + id: mailChannelId1, + model: "mail.channel", + }); + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "new message", + uuid: thread.uuid, + }, + }) + ); + assert.verifySteps( + ["rpc:channel_fetch"], + "Channel should have been fetched but not seen yet" + ); -QUnit.test('should not scroll on receiving new message if the list is initially scrolled anywhere else than bottom (asc order)', async function (assert) { - assert.expect(3); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + document.querySelector(".o_ComposerTextInputView_textarea").focus(), + message: + "should wait until last seen by current partner message id changed after focusing the thread", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }) + ); + assert.verifySteps( + ["rpc:set_last_seen_message"], + "Channel should have been marked as seen after threadView got the focus" + ); + } + ); - const pyEnv = await startServer(); - // Needed partner & user to allow simulation of message reception - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i <= 10; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { afterEvent, click, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); + QUnit.test( + "mark channel as fetched and seen when a new message is loaded if composer is focused [REQUIRE FOCUS]", + async function (assert) { + assert.expect(3); - const thread = messaging.models['Thread'].findFromIdentifyingData({ model: 'mail.channel', id: mailChannelId1 }); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - async func() { - await click(`.o_NotificationListView_preview`); - }, - message: "should wait until channel scrolled initially", - predicate: data => thread === data.threadViewer.thread, - }); - const initialMessageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(initialMessageList), - "should have scrolled to bottom of channel 1 initially" - ); - - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => initialMessageList.scrollTop = 0, - message: "should wait until channel 1 processed manual scroll", - predicate: data => thread === data.threadViewer.thread, - }); - assert.strictEqual( - initialMessageList.scrollTop, - 0, - "should have scrolled to the top of channel 1 manually" - ); - - // simulate receiving a message - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => - messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const resUsersId1 = pyEnv["res.users"].create({ + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, }, - message_content: "hello", - uuid: thread.uuid, + mockRPC(route, args) { + if (args.method === "channel_fetched" && args.args[0] === mailChannelId1) { + throw new Error( + "'channel_fetched' RPC must not be called for created channel as message is directly seen" + ); + } else if (route === "/mail/channel/set_last_seen_message") { + assert.strictEqual( + args.channel_id, + mailChannelId1, + "set_last_seen_message is called on the right channel id" + ); + assert.step("rpc:set_last_seen_message"); + } + }, + }); + await openDiscuss(); + document.querySelector(".o_ComposerTextInputView_textarea").focus(); + // simulate receiving a message + await afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "fdsfsd
", + uuid: messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }).uuid, + }, + }), + message: + "should wait until last seen by current partner message id changed after receiving a message while thread is focused", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }); + assert.verifySteps( + ["rpc:set_last_seen_message"], + "Channel should have been mark as seen directly" + ); + } + ); + + QUnit.test( + "show message subject when subject is not the same as the thread name", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "General", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + subject: "Salutations, voyageur", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + } + ); + + QUnit.test( + "do not show message subject when subject is the same as the thread name", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "Salutations, voyageur", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + subject: "Salutations, voyageur", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject of the message" + ); + } + ); + + QUnit.test( + "[technical] new messages separator on posting message", + async function (assert) { + // technical as we need to remove focus from text input to avoid `set_last_seen_message` call + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "channel", + name: "General", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "first message", + model: "mail.channel", + res_id: mailChannelId1, + }); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display one message in thread initially" + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" + ); + + await insertText(".o_ComposerTextInputView_textarea", "hey !"); + await afterNextRender(() => { + // need to remove focus from text area to avoid set_last_seen_message + document.querySelector(".o_ComposerView_buttonSend").focus(); + document.querySelector(".o_ComposerView_buttonSend").click(); + }); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "should display 2 messages (initial & newly posted), after posting a message" + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "still no separator shown when current partner posted a message" + ); + } + ); + + QUnit.test( + "new messages separator on receiving new message [REQUIRE FOCUS]", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "channel", + name: "General", + uuid: "randomuuid", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "blah", + model: "mail.channel", + res_id: mailChannelId1, + }); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageListView_message", + "should have an initial message" + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" + ); + + document.querySelector(".o_ComposerTextInputView_textarea").blur(); + // simulate receiving a message + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }).uuid, + }, + }), + message: "should wait until new message is received", + predicate: ({ hint, threadViewer }) => { + return ( + threadViewer.thread.id === mailChannelId1 && + threadViewer.thread.model === "mail.channel" && + hint.type === "message-received" + ); + }, + }); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "should now have 2 messages after receiving a new message" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "'new messages' separator should be shown" + ); + + assert.containsOnce( + document.body, + `.o_MessageListView_separatorNewMessages ~ .o_MessageView[data-message-id="${ + mailMessageId1 + 1 + }"]`, + "'new messages' separator should be shown above new message received" + ); + + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + document.querySelector(".o_ComposerTextInputView_textarea").focus(), + message: + "should wait until last seen by current partner message id changed after focusing the thread", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }) + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "'new messages' separator should no longer be shown as last message has been seen" + ); + } + ); + + QUnit.test("new messages separator on posting message", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "channel", + name: "General", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, }, - }), - message: "should wait until channel processed new message hint", - predicate: data => thread === data.threadViewer.thread && data.hint.type === 'message-received', - }); - assert.strictEqual( - document.querySelector('.o_ThreadView_messageList').scrollTop, - 0, - "should not scroll on receiving new message because the list is initially scrolled anywhere else than bottom" - ); -}); + }); + await openDiscuss(); -QUnit.test("delete all attachments of message without content should no longer display the message", async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const irAttachmentId1 = pyEnv['ir.attachment'].create({ - mimetype: 'text/plain', - name: "Blah.txt", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create( - { - attachment_ids: [irAttachmentId1], - model: "mail.channel", - res_id: mailChannelId1, - } - ); - const { afterEvent, click, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - // wait for messages of the thread to be loaded - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + assert.containsNone( + document.body, + ".o_MessageListView_message", + "should have no messages" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be 1 message displayed initially" - ); - - await click(`.o_AttachmentCard[data-id="${ - messaging.models['Attachment'].findFromIdentifyingData({ id: irAttachmentId1 }).localId - }"] .o_AttachmentCard_asideItemUnlink`); - await click('.o_AttachmentDeleteConfirmView_confirmButton'); - assert.containsNone( - document.body, - '.o_MessageView', - "message should no longer be displayed after removing all its attachments (empty content)" - ); -}); - -QUnit.test('delete all attachments of a message with some text content should still keep it displayed', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const irAttachmentId1 = pyEnv['ir.attachment'].create({ - mimetype: 'text/plain', - name: "Blah.txt", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - attachment_ids: [irAttachmentId1], - body: "Some content", - model: "mail.channel", - res_id: mailChannelId1, - }); - const { afterEvent, click, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - // wait for messages of the thread to be loaded - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be 1 message displayed initially" - ); - await click(`.o_AttachmentCard[data-id="${ - messaging.models['Attachment'].findFromIdentifyingData({ id: irAttachmentId1 }).localId - }"] .o_AttachmentCard_asideItemUnlink`); - await click('.o_AttachmentDeleteConfirmView_confirmButton'); - assert.containsOnce( - document.body, - '.o_MessageView', - "message should still be displayed after removing its attachments (non-empty content)" - ); -}); - -QUnit.test('Post a message containing an email address followed by a mention on another line', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "email@odoo.com\n"); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, - "Conversation should have a message that has been posted, which contains partner mention" - ); -}); - -QUnit.test(`Mention a partner with special character (e.g. apostrophe ')`, async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "usatyi@example.com", - name: "Pynya's spokesman", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@Pyn"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@Pynya's spokesman")`, - "Conversation should have a message that has been posted, which contains partner mention" - ); -}); - -QUnit.test('mention 2 different partners that have the same name', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { - email: "partner1@example.com", - name: "TestPartner", - }, - { - email: "partner2@example.com", - name: "TestPartner", - }, - ]); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[0].click()); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[1].click()); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce(document.body, '.o_MessageView_content', 'should have one message after posting it'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, - "message should contain the first partner mention" - ); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId2}"][data-oe-model="res.partner"]:contains("@TestPartner")`, - "message should also contain the second partner mention" - ); -}); - -QUnit.test('mention a channel with space in the name', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General good boy", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message must contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General good boy', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention a channel with "&" in the name', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General & good", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message should contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General & good', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention a channel on a second line when the first line contains #', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General good", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#blabla\n"); - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message should contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General good', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention a channel when replacing the space after the mention by another char', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General good", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - const text = document.querySelector(`.o_ComposerTextInputView_textarea`).value; - document.querySelector(`.o_ComposerTextInputView_textarea`).value = text.slice(0, -1); - await insertText('.o_ComposerTextInputView_textarea', ", test"); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message should contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General good', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention 2 different channels that have the same name', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { - channel_type: 'channel', - group_public_id: false, - name: "my channel", - }, - { - channel_type: 'channel', - name: "my channel", - }, - ]); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - document.querySelector('.o_ComposerTextInputView_textarea').focus(); - await insertText('.o_ComposerTextInputView_textarea', "#my"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[0].click()); - await insertText('.o_ComposerTextInputView_textarea', "#my"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[1].click()); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce(document.body, '.o_MessageView_content', 'should have one message after posting it'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_channel_redirect[data-oe-id="${mailChannelId1}"][data-oe-model="mail.channel"]:contains("#my channel")`, - "message should contain the first channel mention" - ); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_channel_redirect[data-oe-id="${mailChannelId2}"][data-oe-model="mail.channel"]:contains("#my channel")`, - "message should also contain the second channel mention" - ); -}); - -QUnit.test('show empty placeholder when thread contains no message', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until thread becomes loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + await insertText(".o_ComposerTextInputView_textarea", "hey !"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.body, + ".o_MessageView", + "should have the message current partner just posted" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageListView_empty', - "message list empty placeholder should be shown as thread does not contain any messages" - ); - assert.containsNone( - document.body, - '.o_MessageView', - "no message should be shown as thread does not contain any" - ); -}); - -QUnit.test('show empty placeholder when thread contains only empty messages', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - model: "mail.channel", - res_id: mailChannelId1, - }); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "still no separator shown when current partner posted a message" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageListView_empty', - "message list empty placeholder should be shown as thread contain only empty messages" - ); - assert.containsNone( - document.body, - '.o_MessageView', - "no message should be shown as thread contains only empty ones" - ); -}); - -QUnit.test('message with subtype should be displayed (and not considered as empty)', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageSubtypeId1 = pyEnv['mail.message.subtype'].create({ - description: "Task created", - }); - pyEnv['mail.message'].create({ - model: "mail.channel", - res_id: mailChannelId1, - subtype_id: mailMessageSubtypeId1, - }); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until thread becomes loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display 1 message (message with subtype description 'task created')" - ); - assert.strictEqual( - document.body.querySelector('.o_MessageView_content').textContent, - "Task created", - "message should have 'Task created' (from its subtype description)" - ); -}); - -QUnit.test('[technical] message list with a full page of empty messages should show load more if there are other messages', async function (assert) { - // Technical assumptions : - // - /mail/channel/messages fetching exactly 30 messages, - // - empty messages not being displayed - // - auto-load more being triggered on scroll, not automatically when the 30 first messages are empty - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i <= 30; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, }); - } - for (let i = 0; i <= 30; i++) { - pyEnv['mail.message'].create({ - model: "mail.channel", - res_id: mailChannelId1, + + QUnit.test("basic rendering of canceled notification", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Someone" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + message_type: "email", + model: "mail.channel", + res_id: mailChannelId1, + }); + pyEnv["mail.notification"].create({ + failure_type: "SMTP", + mail_message_id: mailMessageId1, + notification_status: "canceled", + notification_type: "email", + res_partner_id: resPartnerId1, + }); + const { afterEvent, click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + + assert.containsOnce( + document.body, + ".o_MessageView_notificationIconClickable", + "should display the notification icon container on the message" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIcon", + "should display the notification icon on the message" + ); + assert.hasClass( + document.querySelector(".o_MessageView_notificationIcon"), + "fa-envelope-o", + "notification icon shown on the message should represent email" + ); + + await click(".o_MessageView_notificationIconClickable"); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView", + "notification popover should be opened after notification has been clicked" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationIcon", + "an icon should be shown in notification popover" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationIcon.fa.fa-trash-o", + "the icon shown in notification popover should be the canceled icon" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationPartnerName", + "partner name should be shown in notification popover" + ); + assert.strictEqual( + document + .querySelector( + ".o_MessageNotificationPopoverContentView_notificationPartnerName" + ) + .textContent.trim(), + "Someone", + "partner name shown in notification popover should be the one concerned by the notification" + ); }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until thread becomes loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + + QUnit.test( + "should scroll to bottom on receiving new message if the list is initially scrolled to bottom (asc order)", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + // Needed partner & user to allow simulation of message reception + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i <= 10; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await click(`.o_MessagingMenu_toggler`); + const thread = messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + async func() { + await click(`.o_NotificationListView_preview`); + }, + message: "should wait until channel scrolled initially", + predicate: (data) => thread === data.threadViewer.thread, + }); + const initialMessageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(initialMessageList), + "should have scrolled to bottom of channel 20 initially" + ); + + // simulate receiving a message + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hello", + uuid: thread.uuid, + }, + }), + message: "should wait until channel scrolled after receiving a message", + predicate: (data) => thread === data.threadViewer.thread, + }); + const messageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(messageList), + "should scroll to bottom on receiving new message because the list is initially scrolled to bottom" + ); + } + ); + + QUnit.test( + "should not scroll on receiving new message if the list is initially scrolled anywhere else than bottom (asc order)", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + // Needed partner & user to allow simulation of message reception + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i <= 10; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + + const thread = messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + async func() { + await click(`.o_NotificationListView_preview`); + }, + message: "should wait until channel scrolled initially", + predicate: (data) => thread === data.threadViewer.thread, + }); + const initialMessageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(initialMessageList), + "should have scrolled to bottom of channel 1 initially" + ); + + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => (initialMessageList.scrollTop = 0), + message: "should wait until channel 1 processed manual scroll", + predicate: (data) => thread === data.threadViewer.thread, + }); + assert.strictEqual( + initialMessageList.scrollTop, + 0, + "should have scrolled to the top of channel 1 manually" + ); + + // simulate receiving a message + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hello", + uuid: thread.uuid, + }, + }), + message: "should wait until channel processed new message hint", + predicate: (data) => + thread === data.threadViewer.thread && + data.hint.type === "message-received", + }); + assert.strictEqual( + document.querySelector(".o_ThreadView_messageList").scrollTop, + 0, + "should not scroll on receiving new message because the list is initially scrolled anywhere else than bottom" + ); + } + ); + + QUnit.test( + "delete all attachments of message without content should no longer display the message", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const irAttachmentId1 = pyEnv["ir.attachment"].create({ + mimetype: "text/plain", + name: "Blah.txt", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [irAttachmentId1], + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, click, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + // wait for messages of the thread to be loaded + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "there should be 1 message displayed initially" + ); + + await click( + `.o_AttachmentCard[data-id="${ + messaging.models["Attachment"].findFromIdentifyingData({ + id: irAttachmentId1, + }).localId + }"] .o_AttachmentCard_asideItemUnlink` + ); + await click(".o_AttachmentDeleteConfirmView_confirmButton"); + assert.containsNone( + document.body, + ".o_MessageView", + "message should no longer be displayed after removing all its attachments (empty content)" + ); + } + ); + + QUnit.test( + "delete all attachments of a message with some text content should still keep it displayed", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const irAttachmentId1 = pyEnv["ir.attachment"].create({ + mimetype: "text/plain", + name: "Blah.txt", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [irAttachmentId1], + body: "Some content", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, click, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + // wait for messages of the thread to be loaded + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "there should be 1 message displayed initially" + ); + + await click( + `.o_AttachmentCard[data-id="${ + messaging.models["Attachment"].findFromIdentifyingData({ + id: irAttachmentId1, + }).localId + }"] .o_AttachmentCard_asideItemUnlink` + ); + await click(".o_AttachmentDeleteConfirmView_confirmButton"); + assert.containsOnce( + document.body, + ".o_MessageView", + "message should still be displayed after removing its attachments (non-empty content)" + ); + } + ); + + QUnit.test( + "Post a message containing an email address followed by a mention on another line", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "email@odoo.com\n"); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, + "Conversation should have a message that has been posted, which contains partner mention" + ); + } + ); + + QUnit.test( + `Mention a partner with special character (e.g. apostrophe ')`, + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "usatyi@example.com", + name: "Pynya's spokesman", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@Pyn"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@Pynya's spokesman")`, + "Conversation should have a message that has been posted, which contains partner mention" + ); + } + ); + + QUnit.test("mention 2 different partners that have the same name", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { + email: "partner1@example.com", + name: "TestPartner", + }, + { + email: "partner2@example.com", + name: "TestPartner", + }, + ]); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[0].click() ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView', - "No message should be shown as all 30 first messages are empty" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_loadMore', - "Load more button should be shown as there are more messages to show" - ); -}); - -QUnit.test('first unseen message should be directly preceded by the new message separator if there is a transient message just before it while composer is not focused [REQUIRE FOCUS]', async function (assert) { - // The goal of removing the focus is to ensure the thread is not marked as seen automatically. - // Indeed that would trigger set_last_seen_message no matter what, which is already covered by other tests. - // The goal of this test is to cover the conditions specific to transient messages, - // and the conditions from focus would otherwise shadow them. - assert.expect(3); - - const pyEnv = await startServer(); - // Needed partner & user to allow simulation of message reception - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - uuid: 'channel20uuid', - }); - const { click, insertText, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - // send a command that leads to receiving a transient message - await insertText('.o_ComposerTextInputView_textarea', "/who"); - await click('.o_ComposerView_buttonSend'); - const transientMessage = messaging.discuss.threadViewer.threadView.messageListView.messageListViewItems[0].message; - - // composer is focused by default, we remove that focus - document.querySelector('.o_ComposerTextInputView_textarea').blur(); - // simulate receiving a message - await afterNextRender(() => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "test", - uuid: 'channel20uuid', - }, - })); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "should display 2 messages (the transient & the received message), after posting a command" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "separator should be shown as a message has been received" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-message-id="${ - transientMessage.id - }"] + .o_MessageListView_separatorNewMessages`, - "separator should be shown just after transient message" - ); -}); - -QUnit.test('composer should be focused automatically after clicking on the send button [REQUIRE FOCUS]', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "Dummy Message"); - await click('.o_ComposerView_buttonSend'); - assert.hasClass( - document.querySelector('.o_ComposerView'), - 'o-focused', - "composer should be focused automatically after clicking on the send button" - ); -}); - -QUnit.test('failure on loading messages should display error', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - return Promise.reject(); - } - }, - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - - assert.containsOnce( - document.body, - '.o_ThreadView_loadingFailed', - "should show loading error message" - ); -}); - -QUnit.test('failure on loading messages should prompt retry button', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - return Promise.reject(); - } - }, - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - - assert.containsOnce( - document.body, - '.o_ThreadView_loadingFailedRetryButton', - "should show a button to allow user to retry loading" - ); -}); - -QUnit.test('failure on loading more messages should not alter message list display', async function (assert) { - assert.expect(1); - - // first call needs to be successful as it is the initial loading of messages - // second call comes from load more and needs to fail in order to show the error alert - // any later call should work so that retry button and load more clicks would now work - let messageFetchShouldFail = false; - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create([...Array(60).keys()].map(() => { - return { - body: 'coucou', - model: "mail.channel", - res_id: mailChannelId1, - }; - })); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - if (messageFetchShouldFail) { - throw new Error(); - } - } - }, - }); - await openDiscuss(); - - messageFetchShouldFail = true; - await click('.o_MessageListView_loadMore'); - assert.containsN( - document.body, - '.o_MessageView', - 30, - "should still show 30 messages as load more has failed" - ); -}); - -QUnit.test('failure on loading more messages should display error and prompt retry button', async function (assert) { - assert.expect(3); - - // first call needs to be successful as it is the initial loading of messages - // second call comes from load more and needs to fail in order to show the error alert - // any later call should work so that retry button and load more clicks would now work - let messageFetchShouldFail = false; - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create([...Array(60).keys()].map(() => { - return { - body: 'coucou', - model: "mail.channel", - res_id: mailChannelId1, - }; - })); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - if (messageFetchShouldFail) { - throw new Error(); - } - } - }, - }); - await openDiscuss(); - - messageFetchShouldFail = true; - await click('.o_MessageListView_loadMore'); - assert.containsOnce( - document.body, - '.o_MessageListView_alertLoadingFailed', - "should show loading error message" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_alertLoadingFailedRetryButton', - "should show loading error message button" - ); - assert.containsNone( - document.body, - '.o_MessageListView_loadMore', - "should not show load more buttton" - ); -}); - -QUnit.test('Retry loading more messages on failed load more messages should load more messages', async function (assert) { - assert.expect(0); - - // first call needs to be successful as it is the initial loading of messages - // second call comes from load more and needs to fail in order to show the error alert - // any later call should work so that retry button and load more clicks would now work - let messageFetchShouldFail = false; - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create([...Array(90).keys()].map(() => { - return { - body: 'coucou', - model: "mail.channel", - res_id: mailChannelId1, - }; - })); - const { afterEvent, click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - if (messageFetchShouldFail) { - return Promise.reject(); - } - } - } - }); - await openDiscuss(); - messageFetchShouldFail = true; - await click('.o_MessageListView_loadMore'); - - messageFetchShouldFail = false; - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_MessageListView_alertLoadingFailedRetryButton').click(), - message: "should wait until channel loaded more messages after clicked on load more", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[1].click() ); - }, + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "should have one message after posting it" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, + "message should contain the first partner mention" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId2}"][data-oe-model="res.partner"]:contains("@TestPartner")`, + "message should also contain the second partner mention" + ); + }); + + QUnit.test("mention a channel with space in the name", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General good boy", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message must contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General good boy", + "link to the channel must contains # + the channel name" + ); + }); + + QUnit.test('mention a channel with "&" in the name', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General & good", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message should contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General & good", + "link to the channel must contains # + the channel name" + ); + }); + + QUnit.test( + "mention a channel on a second line when the first line contains #", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General good", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#blabla\n"); + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message should contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General good", + "link to the channel must contains # + the channel name" + ); + } + ); + + QUnit.test( + "mention a channel when replacing the space after the mention by another char", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General good", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + const text = document.querySelector(`.o_ComposerTextInputView_textarea`).value; + document.querySelector(`.o_ComposerTextInputView_textarea`).value = text.slice( + 0, + -1 + ); + await insertText(".o_ComposerTextInputView_textarea", ", test"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message should contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General good", + "link to the channel must contains # + the channel name" + ); + } + ); + + QUnit.test("mention 2 different channels that have the same name", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { + channel_type: "channel", + group_public_id: false, + name: "my channel", + }, + { + channel_type: "channel", + name: "my channel", + }, + ]); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + document.querySelector(".o_ComposerTextInputView_textarea").focus(); + await insertText(".o_ComposerTextInputView_textarea", "#my"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[0].click() + ); + await insertText(".o_ComposerTextInputView_textarea", "#my"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[1].click() + ); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "should have one message after posting it" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_channel_redirect[data-oe-id="${mailChannelId1}"][data-oe-model="mail.channel"]:contains("#my channel")`, + "message should contain the first channel mention" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_channel_redirect[data-oe-id="${mailChannelId2}"][data-oe-model="mail.channel"]:contains("#my channel")`, + "message should also contain the second channel mention" + ); + }); + + QUnit.test( + "show empty placeholder when thread contains no message", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until thread becomes loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageListView_empty", + "message list empty placeholder should be shown as thread does not contain any messages" + ); + assert.containsNone( + document.body, + ".o_MessageView", + "no message should be shown as thread does not contain any" + ); + } + ); + + QUnit.test( + "show empty placeholder when thread contains only empty messages", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageListView_empty", + "message list empty placeholder should be shown as thread contain only empty messages" + ); + assert.containsNone( + document.body, + ".o_MessageView", + "no message should be shown as thread contains only empty ones" + ); + } + ); + + QUnit.test( + "message with subtype should be displayed (and not considered as empty)", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageSubtypeId1 = pyEnv["mail.message.subtype"].create({ + description: "Task created", + }); + pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + subtype_id: mailMessageSubtypeId1, + }); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until thread becomes loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display 1 message (message with subtype description 'task created')" + ); + assert.strictEqual( + document.body.querySelector(".o_MessageView_content").textContent, + "Task created", + "message should have 'Task created' (from its subtype description)" + ); + } + ); + + QUnit.test( + "[technical] message list with a full page of empty messages should show load more if there are other messages", + async function (assert) { + // Technical assumptions : + // - /mail/channel/messages fetching exactly 30 messages, + // - empty messages not being displayed + // - auto-load more being triggered on scroll, not automatically when the 30 first messages are empty + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i <= 30; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + for (let i = 0; i <= 30; i++) { + pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until thread becomes loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView", + "No message should be shown as all 30 first messages are empty" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_loadMore", + "Load more button should be shown as there are more messages to show" + ); + } + ); + + QUnit.test( + "first unseen message should be directly preceded by the new message separator if there is a transient message just before it while composer is not focused [REQUIRE FOCUS]", + async function (assert) { + // The goal of removing the focus is to ensure the thread is not marked as seen automatically. + // Indeed that would trigger set_last_seen_message no matter what, which is already covered by other tests. + // The goal of this test is to cover the conditions specific to transient messages, + // and the conditions from focus would otherwise shadow them. + assert.expect(3); + + const pyEnv = await startServer(); + // Needed partner & user to allow simulation of message reception + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + uuid: "channel20uuid", + }); + const { click, insertText, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + // send a command that leads to receiving a transient message + await insertText(".o_ComposerTextInputView_textarea", "/who"); + await click(".o_ComposerView_buttonSend"); + const transientMessage = + messaging.discuss.threadViewer.threadView.messageListView + .messageListViewItems[0].message; + + // composer is focused by default, we remove that focus + document.querySelector(".o_ComposerTextInputView_textarea").blur(); + // simulate receiving a message + await afterNextRender(() => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "test", + uuid: "channel20uuid", + }, + }) + ); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "should display 2 messages (the transient & the received message), after posting a command" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "separator should be shown as a message has been received" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-message-id="${transientMessage.id}"] + .o_MessageListView_separatorNewMessages`, + "separator should be shown just after transient message" + ); + } + ); + + QUnit.test( + "composer should be focused automatically after clicking on the send button [REQUIRE FOCUS]", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "Dummy Message"); + await click(".o_ComposerView_buttonSend"); + assert.hasClass( + document.querySelector(".o_ComposerView"), + "o-focused", + "composer should be focused automatically after clicking on the send button" + ); + } + ); + + QUnit.test("failure on loading messages should display error", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + return Promise.reject(); + } + }, + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + + assert.containsOnce( + document.body, + ".o_ThreadView_loadingFailed", + "should show loading error message" + ); + }); + + QUnit.test( + "failure on loading messages should prompt retry button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + return Promise.reject(); + } + }, + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + + assert.containsOnce( + document.body, + ".o_ThreadView_loadingFailedRetryButton", + "should show a button to allow user to retry loading" + ); + } + ); + + QUnit.test( + "failure on loading more messages should not alter message list display", + async function (assert) { + assert.expect(1); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create( + [...Array(60).keys()].map(() => { + return { + body: "coucou", + model: "mail.channel", + res_id: mailChannelId1, + }; + }) + ); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + if (messageFetchShouldFail) { + throw new Error(); + } + } + }, + }); + await openDiscuss(); + + messageFetchShouldFail = true; + await click(".o_MessageListView_loadMore"); + assert.containsN( + document.body, + ".o_MessageView", + 30, + "should still show 30 messages as load more has failed" + ); + } + ); + + QUnit.test( + "failure on loading more messages should display error and prompt retry button", + async function (assert) { + assert.expect(3); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create( + [...Array(60).keys()].map(() => { + return { + body: "coucou", + model: "mail.channel", + res_id: mailChannelId1, + }; + }) + ); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + if (messageFetchShouldFail) { + throw new Error(); + } + } + }, + }); + await openDiscuss(); + + messageFetchShouldFail = true; + await click(".o_MessageListView_loadMore"); + assert.containsOnce( + document.body, + ".o_MessageListView_alertLoadingFailed", + "should show loading error message" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_alertLoadingFailedRetryButton", + "should show loading error message button" + ); + assert.containsNone( + document.body, + ".o_MessageListView_loadMore", + "should not show load more buttton" + ); + } + ); + + QUnit.test( + "Retry loading more messages on failed load more messages should load more messages", + async function (assert) { + assert.expect(0); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create( + [...Array(90).keys()].map(() => { + return { + body: "coucou", + model: "mail.channel", + res_id: mailChannelId1, + }; + }) + ); + const { afterEvent, click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + if (messageFetchShouldFail) { + return Promise.reject(); + } + } + }, + }); + await openDiscuss(); + messageFetchShouldFail = true; + await click(".o_MessageListView_loadMore"); + + messageFetchShouldFail = false; + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => + document + .querySelector(".o_MessageListView_alertLoadingFailedRetryButton") + .click(), + message: + "should wait until channel loaded more messages after clicked on load more", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + } + ); + + QUnit.test( + "highlight the message mentioning the current user inside the channel", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "Test Partner", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "hello @Admin", + model: "mail.channel", + partner_ids: [pyEnv.currentPartnerId], + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.hasClass( + document.querySelector(`.o_MessageListView .o_MessageView`), + "o-highlighted", + "message should be highlighted" + ); + } + ); + + QUnit.test( + "not highlighting the message if not mentioning the current user inside the channel", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "testPartner", + email: "testPartner@odoo.com", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "hello @testPartner", + model: "mail.channel", + partner_ids: [resPartnerId1], + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.doesNotHaveClass( + document.querySelector(`.o_MessageListView .o_MessageView`), + "o-highlighted", + "message should not be highlighted" + ); + } + ); }); }); - -QUnit.test("highlight the message mentioning the current user inside the channel", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "Test Partner", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "hello @Admin", - model: 'mail.channel', - partner_ids: [pyEnv.currentPartnerId], - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.hasClass( - document.querySelector(`.o_MessageListView .o_MessageView`), - 'o-highlighted', - "message should be highlighted" - ); -}); - -QUnit.test("not highlighting the message if not mentioning the current user inside the channel", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "testPartner", - email: "testPartner@odoo.com", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "hello @testPartner", - model: 'mail.channel', - partner_ids: [resPartnerId1], - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.doesNotHaveClass( - document.querySelector(`.o_MessageListView .o_MessageView`), - 'o-highlighted', - "message should not be highlighted" - ); -}); - -}); -}); diff --git a/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js index 40b3f2db3d5..70cac40866a 100644 --- a/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js @@ -1,8 +1,17 @@ /** @odoo-module **/ import { manageMessages } from "@mail/js/tools/debug_manager"; -import { click, getFixture, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; -import { createWebClient, doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers"; +import { + click, + getFixture, + legacyExtraNextTick, + patchWithCleanup, +} from "@web/../tests/helpers/utils"; +import { + createWebClient, + doAction, + getActionManagerServerData, +} from "@web/../tests/webclient/helpers"; import { registry } from "@web/core/registry"; QUnit.module("DebugMenu"); @@ -44,9 +53,7 @@ QUnit.test("Manage Messages", async function (assert) { await legacyExtraNextTick(); await click(target, ".o_debug_manager .dropdown-toggle"); - const dropdownItems = target.querySelectorAll( - ".o_debug_manager .dropdown-menu .dropdown-item" - ); + const dropdownItems = target.querySelectorAll(".o_debug_manager .dropdown-menu .dropdown-item"); assert.strictEqual(dropdownItems.length, 1); assert.strictEqual( dropdownItems[0].innerText.trim(), diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js index 995bccd7b8b..4688029d7a4 100644 --- a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js @@ -1,253 +1,311 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; +import { + click, + getFixture, + legacyExtraNextTick, + patchWithCleanup, + triggerHotkey, +} from "@web/../tests/helpers/utils"; import { registry } from "@web/core/registry"; import { makeLegacyCommandService } from "@web/legacy/utils"; -import core from 'web.core'; -import session from 'web.session'; +import core from "web.core"; +import session from "web.session"; import makeTestEnvironment from "web.test_env"; -import { dom, nextTick } from 'web.test_utils'; +import { dom, nextTick } from "web.test_utils"; let target; -QUnit.module('mail', {}, function () { - QUnit.module('M2XAvatarUserLegacy', { +QUnit.module("mail", {}, function () { + QUnit.module("M2XAvatarUserLegacy", { beforeEach() { target = getFixture(); }, }); - QUnit.test('many2many_avatar_user widget in form view', async function (assert) { + QUnit.test("many2many_avatar_user widget in form view", async function (assert) { assert.expect(2); const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "Partner 1" }); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario", partner_id: resPartnerId1 }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_ids: [resUsersId1] }); const views = { - 'm2x.avatar.user,false,form': '', + "m2x.avatar.user,false,form": + '', }; const { openView } = await start({ serverData: { views }, }); await openView({ - res_model: 'm2x.avatar.user', + res_model: "m2x.avatar.user", res_id: m2xAvatarUserId1, - views: [[false, 'form']], + views: [[false, "form"]], }); - await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); - assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened'); + await dom.click( + document.querySelector(".o_field_many2manytags.avatar .badge .o_m2m_avatar") + ); + assert.containsOnce(document.body, ".o_ChatWindow", "Chat window should be opened"); assert.strictEqual( - document.querySelector('.o_ChatWindowHeaderView_name').textContent, - 'Partner 1', - 'First chat window should be related to partner 1' + document.querySelector(".o_ChatWindowHeaderView_name").textContent, + "Partner 1", + "First chat window should be related to partner 1" ); }); - QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2one_avatar_user widget edited by the smart action "Assign to..."', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + { name: "Yoshi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") + const views = { + "m2x.avatar.user,false,form": + '', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario"); - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") - assert.ok(idx >= 0); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign to ...ALT + I"); + assert.ok(idx >= 0); - await click([...target.querySelectorAll(".o_command")][idx]) - await nextTick(); - assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ - "Your Company, Mitchell Admin", - "Public user", - "Mario", - "Luigi", - "Yoshi", - ]) - await click(target, "#o_command_3") - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") - }); + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Your Company, Mitchell Admin", "Public user", "Mario", "Luigi", "Yoshi"] + ); + await click(target, "#o_command_3"); + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi"); + } + ); - QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2one_avatar_user widget edited by the smart action "Assign to me"', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - patchWithCleanup(session, { user_id: [resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + patchWithCleanup(session, { user_id: [resUsersId2] }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I") - assert.ok(idx >= 0); + const views = { + "m2x.avatar.user,false,form": + '', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario"); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign/unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); - // Assign me (Luigi) - triggerHotkey("alt+shift+i") - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi"); - // Unassign me - triggerHotkey("control+k"); - await nextTick(); - await click([...target.querySelectorAll(".o_command")][idx]) - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "") - }); + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]); + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, ""); + } + ); - QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2many_avatar_user widget edited by the smart action "Assign to..."', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Yoshi" }, + { name: "Luigi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ + user_ids: [resUsersId1, resUsersId2], + }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); + const views = { + "m2x.avatar.user,false,form": + '', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") - assert.ok(idx >= 0); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign to ...ALT + I"); + assert.ok(idx >= 0); - await click([...target.querySelectorAll(".o_command")][idx]) - await nextTick(); - assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ - "Your Company, Mitchell Admin", - "Public user", - "Luigi" - ]); + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Your Company, Mitchell Admin", "Public user", "Luigi"] + ); - await click(target, "#o_command_2"); - await legacyExtraNextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent); - assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); - }); + await click(target, "#o_command_2"); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + } + ); - QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2many_avatar_user widget edited by the smart action "Assign to me"', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2, resUsersId3] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] }); - patchWithCleanup(session, { user_id: [resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2, resUsersId3] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + { name: "Yoshi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ + user_ids: [resUsersId1, resUsersId3], + }); + patchWithCleanup(session, { user_id: [resUsersId2] }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); + const views = { + "m2x.avatar.user,false,form": + '', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); - triggerHotkey("control+k"); - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I"); - assert.ok(idx >= 0); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign/unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); - // Assign me (Luigi) - triggerHotkey("alt+shift+i"); - await legacyExtraNextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); - // Unassign me - triggerHotkey("control+k"); - await nextTick(); - await click([...target.querySelectorAll(".o_command")][idx]); - await legacyExtraNextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); - }); + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + } + ); - QUnit.test('avatar_user widget displays the appropriate user image in form view', async function (assert) { - assert.expect(1); + QUnit.test( + "avatar_user widget displays the appropriate user image in form view", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); - const views = { - 'm2x.avatar.user,false,form': '', - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'm2x.avatar.user', - res_id: m2xAvatarUserId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), - `/web/image/res.users/${resUsersId1}/avatar_128`, - 'Should have correct avatar image' - ); - }); + const pyEnv = await startServer(); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario" }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_ids: [resUsersId1] }); + const views = { + "m2x.avatar.user,false,form": + '', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "m2x.avatar.user", + res_id: m2xAvatarUserId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document + .querySelector(".o_field_many2manytags.avatar.o_field_widget .badge img") + .getAttribute("data-src"), + `/web/image/res.users/${resUsersId1}/avatar_128`, + "Should have correct avatar image" + ); + } + ); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js index 3a3bf25a643..4a4b8603df3 100644 --- a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js @@ -1,90 +1,96 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; import { click, getFixture, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; import { registry } from "@web/core/registry"; import { session } from "@web/session"; -import { dom, nextTick } from 'web.test_utils'; +import { dom, nextTick } from "web.test_utils"; import { popoverService } from "@web/core/popover/popover_service"; import { tooltipService } from "@web/core/tooltip/tooltip_service"; let target; -QUnit.module('mail', {}, function () { - QUnit.module('M2XAvatarUser', { +QUnit.module("mail", {}, function () { + QUnit.module("M2XAvatarUser", { beforeEach() { target = getFixture(); }, }); - QUnit.test('many2one_avatar_user widget in list view', async function (assert) { + QUnit.test("many2one_avatar_user widget in list view", async function (assert) { assert.expect(2); const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); - pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "Partner 1" }); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario", partner_id: resPartnerId1 }); + pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); const views = { - 'm2x.avatar.user,false,list': 'Test
", + date: moment(str_to_datetime("2019-05-05 10:00:00")), + id: 4000, + isNeedaction: true, + isStarred: true, + originThread: thread, + }); + + assert.ok(messaging.models["Partner"].findFromIdentifyingData({ id: 5 })); + assert.ok( + messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }) + ); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.ok(messaging.models["Message"].findFromIdentifyingData({ id: 4000 })); + + assert.ok(message); + assert.strictEqual( + messaging.models["Message"].findFromIdentifyingData({ id: 4000 }), + message + ); + assert.strictEqual(message.body, "Test
"); + assert.ok(message.date instanceof moment); + assert.strictEqual( + moment(message.date).utc().format("YYYY-MM-DD hh:mm:ss"), + "2019-05-05 10:00:00" + ); + assert.strictEqual(message.id, 4000); + assert.strictEqual( + message.originThread, + messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }) + ); + assert.ok( + message.threads.includes( + messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }) + ) + ); + // from partnerId being in needaction_partner_ids + assert.ok(message.threads.includes(messaging.inbox.thread)); + // from partnerId being in starred_partner_ids + assert.ok(message.threads.includes(messaging.starred.thread)); + const attachment = messaging.models["Attachment"].findFromIdentifyingData({ id: 750 }); + assert.ok(attachment); + assert.strictEqual(attachment.filename, "test.txt"); + assert.strictEqual(attachment.id, 750); + assert.notOk(attachment.isUploading); + assert.strictEqual(attachment.mimetype, "text/plain"); + assert.strictEqual(attachment.name, "test.txt"); + const channel = messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }); + assert.ok(channel); + assert.strictEqual(channel.model, "mail.channel"); + assert.strictEqual(channel.id, 100); + assert.strictEqual(channel.name, "General"); + const partner = messaging.models["Partner"].findFromIdentifyingData({ id: 5 }); + assert.ok(partner); + assert.strictEqual(partner.displayName, "Demo"); + assert.strictEqual(partner.id, 5); + }); + + QUnit.test("message without body should be considered empty", async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ id: 11 }); + assert.ok(message.isEmpty); + }); + + QUnit.test('message with body "" should be considered empty', async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "", id: 11 }); + assert.ok(message.isEmpty); + }); + + QUnit.test( + 'message with body "" should be considered empty', + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "", id: 11 }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + 'message with body "\n
" should be considered empty`, + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "\n
", id: 11 }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + String.raw`message with body "\r\n\r\n
" should be considered empty`, + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ + body: "\r\n\r\n
", + id: 11, + }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + 'message with body "" should be considered empty', + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ + body: "
", + id: 11, + }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + `message with body "
Test
", - date: moment(str_to_datetime("2019-05-05 10:00:00")), - id: 4000, - isNeedaction: true, - isStarred: true, - originThread: thread, - }); - - assert.ok(messaging.models['Partner'].findFromIdentifyingData({ id: 5 })); - assert.ok(messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - })); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.ok(messaging.models['Message'].findFromIdentifyingData({ id: 4000 })); - - assert.ok(message); - assert.strictEqual(messaging.models['Message'].findFromIdentifyingData({ id: 4000 }), message); - assert.strictEqual(message.body, "Test
"); - assert.ok(message.date instanceof moment); - assert.strictEqual( - moment(message.date).utc().format('YYYY-MM-DD hh:mm:ss'), - "2019-05-05 10:00:00" - ); - assert.strictEqual(message.id, 4000); - assert.strictEqual(message.originThread, messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - })); - assert.ok( - message.threads.includes(messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - })) - ); - // from partnerId being in needaction_partner_ids - assert.ok(message.threads.includes(messaging.inbox.thread)); - // from partnerId being in starred_partner_ids - assert.ok(message.threads.includes(messaging.starred.thread)); - const attachment = messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }); - assert.ok(attachment); - assert.strictEqual(attachment.filename, "test.txt"); - assert.strictEqual(attachment.id, 750); - assert.notOk(attachment.isUploading); - assert.strictEqual(attachment.mimetype, 'text/plain'); - assert.strictEqual(attachment.name, "test.txt"); - const channel = messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - }); - assert.ok(channel); - assert.strictEqual(channel.model, 'mail.channel'); - assert.strictEqual(channel.id, 100); - assert.strictEqual(channel.name, "General"); - const partner = messaging.models['Partner'].findFromIdentifyingData({ id: 5 }); - assert.ok(partner); - assert.strictEqual(partner.displayName, "Demo"); - assert.strictEqual(partner.id, 5); -}); - -QUnit.test('message without body should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "\n
" should be considered empty`, async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "\n
", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test(String.raw`message with body "\r\n\r\n
" should be considered empty`, async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "\r\n\r\n
", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "
", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test(`message with body "
some text https://somelink.com and again https://somelink2.com ...
"; + const linkified = utils.parseAndTransform(content, utils.addLink); + const fragment = document.createDocumentFragment(); + const div = document.createElement("div"); + fragment.appendChild(div); + div.innerHTML = linkified; + assert.strictEqual( + div.textContent, + "some text https://somelink.com and again https://somelink2.com ...", + "linkified text should have same text content as non-linkified version" + ); + assert.strictEqual( + div.querySelectorAll(":scope a").length, + 2, + "linkified text should have 2 tags" + ); + assert.strictEqual( + div.querySelectorAll(":scope a")[0].textContent, + "https://somelink.com", + "text content of 1st link should be equivalent to its non-linkified version" + ); + assert.strictEqual( + div.querySelectorAll(":scope a")[1].textContent, + "https://somelink2.com", + "text content of 2nd link should be equivalent to its non-linkified version" + ); }); }); - -QUnit.test('addLink: linkify inside text node (1 occurrence)', function (assert) { - assert.expect(5); - - const content = 'some text https://somelink.com
'; - const linkified = utils.parseAndTransform(content, utils.addLink); - assert.ok( - linkified.startsWith(''), - "linkified text should end with closing '' tag" - ); - - // 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; - assert.strictEqual( - div.textContent, - 'some text https://somelink.com', - "linkified text should have same text content as non-linkified version" - ); - assert.strictEqual( - div.querySelectorAll(':scope a').length, - 1, - "linkified text should have an tag" - ); - assert.strictEqual( - div.querySelector(':scope a').textContent, - 'https://somelink.com', - "text content of link should be equivalent of its non-linkified version" - ); -}); - -QUnit.test('addLink: linkify inside text node (2 occurrences)', function (assert) { - assert.expect(4); - - // linkify may add some attributes. Since we do not care of their exact - // stringified representation, we continue deeper assertion with query - // selectors. - const content = 'some text https://somelink.com and again https://somelink2.com ...
'; - const linkified = utils.parseAndTransform(content, utils.addLink); - const fragment = document.createDocumentFragment(); - const div = document.createElement('div'); - fragment.appendChild(div); - div.innerHTML = linkified; - assert.strictEqual( - div.textContent, - 'some text https://somelink.com and again https://somelink2.com ...', - "linkified text should have same text content as non-linkified version" - ); - assert.strictEqual( - div.querySelectorAll(':scope a').length, - 2, - "linkified text should have 2 tags" - ); - assert.strictEqual( - div.querySelectorAll(':scope a')[0].textContent, - 'https://somelink.com', - "text content of 1st link should be equivalent to its non-linkified version" - ); - assert.strictEqual( - div.querySelectorAll(':scope a')[1].textContent, - 'https://somelink2.com', - "text content of 2nd link should be equivalent to its non-linkified version" - ); -}); - -}); diff --git a/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js b/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js index 76de0fa74da..6258e2241d9 100644 --- a/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js @@ -1,174 +1,173 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; -import { nextTick } from '@mail/utils/utils'; +import { start } from "@mail/../tests/helpers/test_utils"; +import { nextTick } from "@mail/utils/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('utils', {}, function () { -QUnit.module('throttle', {}, function () { -QUnit.module('throttle_tests.js', {}); +QUnit.module("mail", {}, function () { + QUnit.module("utils", {}, function () { + QUnit.module("throttle", {}, function () { + QUnit.module("throttle_tests.js", {}); -QUnit.test('single call', async function (assert) { - assert.expect(3); + QUnit.test("single call", async function (assert) { + assert.expect(3); - const { advanceTime, messaging } = await start({ - hasTimeControl: true, + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let hasInvokedFunc = false; + const throttle = messaging.models["Throttle"].insert({ + func: () => (hasInvokedFunc = true), + qunitTestOwner1: {}, + }); + assert.notOk( + hasInvokedFunc, + "func should not have been invoked on immediate throttle initialization" + ); + + await advanceTime(0); + assert.notOk( + hasInvokedFunc, + "func should not have been invoked from throttle initialization after 0ms" + ); + + throttle.do(); + await nextTick(); + assert.ok( + hasInvokedFunc, + "func should have been immediately invoked on first throttle call" + ); + }); + + QUnit.test("2nd (throttled) call", async function (assert) { + assert.expect(4); + + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let funcCalledAmount = 0; + const throttle = messaging.models["Throttle"].insert({ + func: () => funcCalledAmount++, + qunitTestOwner2: {}, + }); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "throttle call return should forward result of inner func 1" + ); + + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" + ); + + await advanceTime(999); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" + ); + + await advanceTime(1); + assert.strictEqual( + funcCalledAmount, + 2, + "throttle call return should forward result of inner func 2" + ); + }); + + QUnit.test("throttled call reinvocation", async function (assert) { + assert.expect(4); + + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let funcCalledAmount = 0; + const throttle = messaging.models["Throttle"].insert({ + func: () => funcCalledAmount++, + qunitTestOwner2: {}, + }); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "throttle call return should forward result of inner func 1" + ); + + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" + ); + + await advanceTime(999); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" + ); + + throttle.do(); + await nextTick(); + await advanceTime(1); + assert.strictEqual( + funcCalledAmount, + 2, + "throttle call return should forward result of inner func 2" + ); + }); + + QUnit.test("clear throttled call", async function (assert) { + assert.expect(4); + + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let funcCalledAmount = 0; + const throttle = messaging.models["Throttle"].insert({ + func: () => funcCalledAmount++, + qunitTestOwner2: {}, + }); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should have been invoked on 1st call (immediate return)" + ); + + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" + ); + + await advanceTime(500); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been invoked after 500ms of 2nd call (throttled with 1s internal clock)" + ); + + throttle.clear(); + await nextTick(); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 2, + "3rd throttle function call should have invoke inner function immediately (`.clear()` flushes throttle)" + ); + }); + }); }); - let hasInvokedFunc = false; - const throttle = messaging.models['Throttle'].insert({ - func: () => hasInvokedFunc = true, - qunitTestOwner1: {}, - }); - assert.notOk( - hasInvokedFunc, - "func should not have been invoked on immediate throttle initialization" - ); - - await advanceTime(0); - assert.notOk( - hasInvokedFunc, - "func should not have been invoked from throttle initialization after 0ms" - ); - - throttle.do(); - await nextTick(); - assert.ok( - hasInvokedFunc, - "func should have been immediately invoked on first throttle call" - ); -}); - -QUnit.test('2nd (throttled) call', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ - hasTimeControl: true, - }); - let funcCalledAmount = 0; - const throttle = messaging.models['Throttle'].insert({ - func: () => funcCalledAmount++, - qunitTestOwner2: {}, - }); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "throttle call return should forward result of inner func 1" - ); - - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" - ); - - await advanceTime(999); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" - ); - - await advanceTime(1); - assert.strictEqual( - funcCalledAmount, - 2, - "throttle call return should forward result of inner func 2" - ); -}); - -QUnit.test('throttled call reinvocation', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ - hasTimeControl: true, - }); - let funcCalledAmount = 0; - const throttle = messaging.models['Throttle'].insert({ - func: () => funcCalledAmount++, - qunitTestOwner2: {}, - }); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "throttle call return should forward result of inner func 1" - ); - - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" - ); - - await advanceTime(999); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" - ); - - throttle.do(); - await nextTick(); - await advanceTime(1); - assert.strictEqual( - funcCalledAmount, - 2, - "throttle call return should forward result of inner func 2" - ); -}); - -QUnit.test('clear throttled call', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ - hasTimeControl: true, - }); - let funcCalledAmount = 0; - const throttle = messaging.models['Throttle'].insert({ - func: () => funcCalledAmount++, - qunitTestOwner2: {}, - }); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should have been invoked on 1st call (immediate return)" - ); - - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" - ); - - await advanceTime(500); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been invoked after 500ms of 2nd call (throttled with 1s internal clock)" - ); - - throttle.clear(); - await nextTick(); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 2, - "3rd throttle function call should have invoke inner function immediately (`.clear()` flushes throttle)" - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js b/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js index 3c02afdd39e..9bced8aba05 100644 --- a/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js @@ -1,68 +1,58 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('utils', {}, function () { -QUnit.module('timer', {}, function () { -QUnit.module('timer_tests.js', {}); +QUnit.module("mail", {}, function () { + QUnit.module("utils", {}, function () { + QUnit.module("timer", {}, function () { + QUnit.module("timer_tests.js", {}); -QUnit.test('timer insert (duration: 0ms)', async function (assert) { - assert.expect(2); + QUnit.test("timer insert (duration: 0ms)", async function (assert) { + assert.expect(2); - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - const timer = messaging.models['Timer'].insert({ - qunitTestOwner1: {}, + const { advanceTime, messaging } = await start({ hasTimeControl: true }); + const timer = messaging.models["Timer"].insert({ + qunitTestOwner1: {}, + }); + assert.ok( + timer.timeoutId, + "timer should not have timed out immediately after insert" + ); + + await advanceTime(0); + assert.notOk(timer.timeoutId, "timer should have timed out on insert after 0ms"); + }); + + QUnit.test("timer insert (duration: 1000s)", async function (assert) { + assert.expect(5); + + const { advanceTime, messaging } = await start({ hasTimeControl: true }); + const timer = messaging.models["Timer"].insert({ + qunitTestOwner2: {}, + }); + assert.ok( + timer.timeoutId, + "timer should not have timed out immediately after insert" + ); + + await advanceTime(0); + assert.ok(timer.timeoutId, "timer should not have timed out on insert after 0ms"); + + await advanceTime(1000); + assert.ok( + timer.timeoutId, + "timer should not have timed out on insert after 1000ms" + ); + + await advanceTime(998 * 1000 + 999); + assert.ok( + timer.timeoutId, + "timer should not have timed out on insert after 9999ms" + ); + + await advanceTime(1); + assert.notOk(timer.timeoutId, "timer should have timed out on insert after 10s"); + }); + }); }); - assert.ok( - timer.timeoutId, - "timer should not have timed out immediately after insert" - ); - - await advanceTime(0); - assert.notOk( - timer.timeoutId, - "timer should have timed out on insert after 0ms" - ); -}); - -QUnit.test('timer insert (duration: 1000s)', async function (assert) { - assert.expect(5); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - const timer = messaging.models['Timer'].insert({ - qunitTestOwner2: {}, - }); - assert.ok( - timer.timeoutId, - "timer should not have timed out immediately after insert" - ); - - await advanceTime(0); - assert.ok( - timer.timeoutId, - "timer should not have timed out on insert after 0ms" - ); - - await advanceTime(1000); - assert.ok( - timer.timeoutId, - "timer should not have timed out on insert after 1000ms" - ); - - await advanceTime(998 * 1000 + 999); - assert.ok( - timer.timeoutId, - "timer should not have timed out on insert after 9999ms" - ); - - await advanceTime(1); - assert.notOk( - timer.timeoutId, - "timer should have timed out on insert after 10s" - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js b/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js index 408cab6ae13..31cb7d27f36 100644 --- a/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; -import { editSearchBar } from '@web/../tests/core/commands/command_service_tests'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; +import { editSearchBar } from "@web/../tests/core/commands/command_service_tests"; import { click, nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; import { commandService } from "@web/core/commands/command_service"; import { registry } from "@web/core/registry"; @@ -9,66 +9,78 @@ import { registry } from "@web/core/registry"; const serviceRegistry = registry.category("services"); const commandSetupRegistry = registry.category("command_setup"); -QUnit.module('mail', {}, function () { - QUnit.module('webclient', function () { - QUnit.module('commands', function () { - QUnit.module('mail_providers_tests.js', { - beforeEach() { - serviceRegistry.add("command", commandService); - registry.category("command_categories").add("default", { label: ("default") }); - }, - }); +QUnit.module("mail", {}, function () { + QUnit.module("webclient", function () { + QUnit.module("commands", function () { + QUnit.module("mail_providers_tests.js", { + beforeEach() { + serviceRegistry.add("command", commandService); + registry.category("command_categories").add("default", { label: "default" }); + }, + }); - QUnit.test('open the chatWindow of a user from the command palette', async function (assert) { - assert.expect(1); + QUnit.test( + "open the chatWindow of a user from the command palette", + async function (assert) { + assert.expect(1); - const { advanceTime } = await start({ - hasTimeControl: true, + const { advanceTime } = await start({ + hasTimeControl: true, + }); + triggerHotkey("control+k"); + await nextTick(); + + // Switch to partners + await editSearchBar("@"); + await afterNextRender(() => + advanceTime(commandSetupRegistry.get("@").debounceDelay) + ); + + await afterNextRender(() => click(document.body, ".o_command.focused")); + assert.containsOnce(document.body, ".o_ChatWindow"); + } + ); + + QUnit.test( + "open the chatWindow of a channel from the command palette", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + name: "general", + }); + pyEnv["mail.channel"].create({ + name: "project", + }); + const { advanceTime } = await start({ + hasTimeControl: true, + }); + triggerHotkey("control+k"); + await nextTick(); + + // Switch to channels + await editSearchBar("#"); + await afterNextRender(() => + advanceTime(commandSetupRegistry.get("#").debounceDelay) + ); + + assert.deepEqual( + [...document.querySelectorAll(".o_command_palette .o_command")].map( + (el) => el.textContent + ), + ["general", "project"] + ); + + await afterNextRender(() => click(document.body, ".o_command.focused")); + assert.containsOnce(document.body, ".o_ChatWindow"); + assert.strictEqual( + document.querySelector(".o_ChatWindow .o_ChatWindowHeaderView_name") + .textContent, + "general" + ); + } + ); }); - triggerHotkey("control+k"); - await nextTick(); - - // Switch to partners - await editSearchBar("@"); - await afterNextRender(() => advanceTime(commandSetupRegistry.get('@').debounceDelay)); - - await afterNextRender(() => click(document.body, ".o_command.focused")); - assert.containsOnce(document.body, ".o_ChatWindow"); - }); - - QUnit.test('open the chatWindow of a channel from the command palette', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - name: "general", - }); - pyEnv['mail.channel'].create({ - name: "project", - }); - const { advanceTime } = await start({ - hasTimeControl: true, - }); - triggerHotkey("control+k"); - await nextTick(); - - // Switch to channels - await editSearchBar("#"); - await afterNextRender(() => advanceTime(commandSetupRegistry.get('#').debounceDelay)); - - assert.deepEqual( - [...document.querySelectorAll(".o_command_palette .o_command")].map((el) => el.textContent), - [ - "general", - "project" - ], - ); - - await afterNextRender(() => click(document.body, ".o_command.focused")); - assert.containsOnce(document.body, ".o_ChatWindow"); - assert.strictEqual(document.querySelector(".o_ChatWindow .o_ChatWindowHeaderView_name").textContent, "general"); - }); - - }); }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js index e5ad9ce941d..0b93da4fa89 100644 --- a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js @@ -1,503 +1,440 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size'; +import { makeDeferred } from "@mail/utils/deferred"; +import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; import { afterNextRender, isScrolledToBottom, nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import fieldRegistry from 'web.field_registry'; -import { dom, makeTestPromise, nextTick } from 'web.test_utils'; +import fieldRegistry from "web.field_registry"; +import { dom, makeTestPromise, nextTick } from "web.test_utils"; import { registerCleanup } from "@web/../tests/helpers/cleanup"; const { triggerEvent } = dom; -QUnit.module('mail', {}, function () { -QUnit.module('widgets', {}, function () { -QUnit.module('form_renderer_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("widgets", {}, function () { + QUnit.module("form_renderer_tests.js"); -QUnit.test('[technical] spinner when messaging is not created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(3); + QUnit.test("[technical] spinner when messaging is not created", async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(3); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation - serverData: { views }, - waitUntilMessagingCondition: 'none', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterContainer', - "should display chatter container even when messaging is not created yet" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not created" - ); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet created" - ); -}); - -QUnit.test('[technical] keep spinner on transition from messaging non-created to messaging created (and non-initialized)', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(4); - - const messaginginitializedDeferred = makeTestPromise(); - const pyEnv = await startServer(); - const messagingBeforeCreationDeferred = makeDeferred(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - messagingBeforeCreationDeferred, - async mockRPC(route, args) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized - } - }, - serverData: { views }, - waitUntilMessagingCondition: 'none', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet created" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not created" - ); - - // simulate messaging become created - messagingBeforeCreationDeferred.resolve(); - await nextAnimationFrame(); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should still display spinner when messaging is created but not initialized" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should still not display any chatter when messaging not initialized" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); - -QUnit.test('spinner when messaging is created but not initialized', async function (assert) { - assert.expect(3); - - const messaginginitializedDeferred = makeTestPromise(); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - async mockRPC(route, args) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized - } - }, - serverData: { views }, - waitUntilMessagingCondition: 'created', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterContainer', - "should display chatter container even when messaging is not fully initialized" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not initialized" - ); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet initialized" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); - -QUnit.test('transition non-initialized messaging to initialized messaging: display spinner then chatter', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const messagingBeforeInitializationDeferred = makeDeferred(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - async mockRPC(route, args) { - if (route === '/mail/init_messaging') { - await messagingBeforeInitializationDeferred; - } - }, - serverData: { views }, - waitUntilMessagingCondition: 'created', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet initialized" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not initialized" - ); - - // Simulate messaging becomes initialized - await afterNextRender(() => messagingBeforeInitializationDeferred.resolve()); - assert.containsNone( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should now display chatter when messaging becomes initialized" - ); -}); - -QUnit.test('basic chatter rendering', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "there should be a chatter" - ); -}); - -QUnit.test('basic chatter rendering without followers', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar', - "there should be a chatter topbar" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonAddAttachments', - "there should be an attachment button" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonScheduleActivity', - "there should be a schedule activity button" - ); - assert.containsNone( - document.body, - '.o_FollowerListMenuView', - "there should be no followers menu because the 'message_follower_ids' field is not present in 'oe_chatter'" - ); - assert.containsOnce( - document.body, - '.o_Chatter_thread', - "there should be a thread" - ); -}); - -QUnit.test('basic chatter rendering without activities', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar', - "there should be a chatter topbar" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonAddAttachments', - "there should be an attachment button" - ); - assert.containsNone( - document.body, - '.o_ChatterTopbar_buttonScheduleActivity', - "there should be no schedule activity button because the 'activity_ids' field is not present in 'oe_chatter'" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "there should be a followers menu" - ); - assert.containsOnce( - document.body, - '.o_Chatter_thread', - "there should be a thread" - ); -}); - -QUnit.test('basic chatter rendering without messages', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - ``, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar', - "there should be a chatter topbar" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonAddAttachments', - "there should be an attachment button" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonScheduleActivity', - "there should be a schedule activity button" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "there should be a followers menu" - ); - assert.containsNone( - document.body, - '.o_Chatter_thread', - "there should be no thread because the 'message_ids' field is not present in 'oe_chatter'" - ); -}); - -QUnit.test('chatter updating', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { display_name: "first partner" }, - { display_name: "second partner" }, - ]); - pyEnv['mail.message'].create({ body: "not empty", model: 'res.partner', res_id: resPartnerId2 }); - const views = { - 'res.partner,false,form': - ``, - }; - const { afterEvent, openFormView } = await start({ - serverData: { views }, - }); - await openFormView( - { - res_model: 'res.partner', - res_id: resPartnerId1, - }, - { - props: { - resIds: [resPartnerId1, resPartnerId2], - }, - } - ); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_pager_next').click(), - message: "should wait until partner 12 thread loaded messages after clicking on next", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'res.partner' && - threadViewer.thread.id === resPartnerId2 + }; + const { openView } = await start({ + messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation + serverData: { views }, + waitUntilMessagingCondition: "none", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterContainer", + "should display chatter container even when messaging is not created yet" ); - }, - })); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be a message in partner 12 thread" - ); -}); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not created" + ); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet created" + ); + }); -QUnit.test('chatter should become enabled when creation done', async function (assert) { - assert.expect(10); + QUnit.test( + "[technical] keep spinner on transition from messaging non-created to messaging created (and non-initialized)", + async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(4); - const views = { - 'res.partner,false,form': - ``, + }; + const { openView } = await start({ + messagingBeforeCreationDeferred, + async mockRPC(route, args) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + serverData: { views }, + waitUntilMessagingCondition: "none", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet created" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not created" + ); + + // simulate messaging become created + messagingBeforeCreationDeferred.resolve(); + await nextAnimationFrame(); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should still display spinner when messaging is created but not initialized" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should still not display any chatter when messaging not initialized" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + } + ); + + QUnit.test( + "spinner when messaging is created but not initialized", + async function (assert) { + assert.expect(3); + + const messaginginitializedDeferred = makeTestPromise(); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "second partner", + }); + const views = { + "res.partner,false,form": ``, + }; + const { openView } = await start({ + async mockRPC(route, args) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + serverData: { views }, + waitUntilMessagingCondition: "created", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterContainer", + "should display chatter container even when messaging is not fully initialized" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not initialized" + ); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet initialized" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + } + ); + + QUnit.test( + "transition non-initialized messaging to initialized messaging: display spinner then chatter", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const messagingBeforeInitializationDeferred = makeDeferred(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "second partner", + }); + const views = { + "res.partner,false,form": ``, + }; + const { openView } = await start({ + async mockRPC(route, args) { + if (route === "/mail/init_messaging") { + await messagingBeforeInitializationDeferred; + } + }, + serverData: { views }, + waitUntilMessagingCondition: "created", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet initialized" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not initialized" + ); + + // Simulate messaging becomes initialized + await afterNextRender(() => messagingBeforeInitializationDeferred.resolve()); + assert.containsNone( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should now display chatter when messaging becomes initialized" + ); + } + ); + + QUnit.test("basic chatter rendering", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": ``, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "there should be a chatter" + ); + }); + + QUnit.test("basic chatter rendering without followers", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": ``, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce( + document.body, + ".o_ChatterTopbar", + "there should be a chatter topbar" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonAddAttachments", + "there should be an attachment button" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonScheduleActivity", + "there should be a schedule activity button" + ); + assert.containsNone( + document.body, + ".o_FollowerListMenuView", + "there should be no followers menu because the 'message_follower_ids' field is not present in 'oe_chatter'" + ); + assert.containsOnce(document.body, ".o_Chatter_thread", "there should be a thread"); + }); + + QUnit.test("basic chatter rendering without activities", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": ``, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce( + document.body, + ".o_ChatterTopbar", + "there should be a chatter topbar" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonAddAttachments", + "there should be an attachment button" + ); + assert.containsNone( + document.body, + ".o_ChatterTopbar_buttonScheduleActivity", + "there should be no schedule activity button because the 'activity_ids' field is not present in 'oe_chatter'" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "there should be a followers menu" + ); + assert.containsOnce(document.body, ".o_Chatter_thread", "there should be a thread"); + }); + + QUnit.test("basic chatter rendering without messages", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": ``, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce( + document.body, + ".o_ChatterTopbar", + "there should be a chatter topbar" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonAddAttachments", + "there should be an attachment button" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonScheduleActivity", + "there should be a schedule activity button" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "there should be a followers menu" + ); + assert.containsNone( + document.body, + ".o_Chatter_thread", + "there should be no thread because the 'message_ids' field is not present in 'oe_chatter'" + ); + }); + + QUnit.test("chatter updating", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { display_name: "first partner" }, + { display_name: "second partner" }, + ]); + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId2, + }); + const views = { + "res.partner,false,form": `