[IMP] mail: rename ComposerSuggestion to ComposerSuggestionView

Part of task-2741982

Part-of: odoo/odoo#95341
This commit is contained in:
Sébastien Theys
2022-07-08 18:44:57 +02:00
parent 2f561252d7
commit 2943b95673
20 changed files with 204 additions and 203 deletions
@@ -4,7 +4,7 @@ import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ComposerSuggestionList extends Component {
export class ComposerSuggestionListView extends Component {
//--------------------------------------------------------------------------
// Public
@@ -19,9 +19,9 @@ export class ComposerSuggestionList extends Component {
}
Object.assign(ComposerSuggestionList, {
Object.assign(ComposerSuggestionListView, {
props: { record: Object },
template: 'mail.ComposerSuggestionList',
template: 'mail.ComposerSuggestionListView',
});
registerMessagingComponent(ComposerSuggestionList);
registerMessagingComponent(ComposerSuggestionListView);
@@ -2,7 +2,7 @@
// Layout
// ------------------------------------------------------------------
.o_ComposerSuggestionList_list {
.o_ComposerSuggestionListView_list {
// prevent from overflowing chat window, must be smaller than its height
// minus the max height taken by composer and attachment list
max-height: 150px;
@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ComposerSuggestionList" owl="1">
<t t-name="mail.ComposerSuggestionListView" owl="1">
<t t-if="composerSuggestionListView">
<div class="o_ComposerSuggestionList position-absolute w-100" t-att-class="{ 'o-lowPosition bottom-0': composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition }" t-attf-class="{{ className }}" t-ref="root">
<div class="o_ComposerSuggestionList_drop w-100" t-att-class="{ 'dropdown': composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition, 'dropup': !composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition }">
<div class="o_ComposerSuggestionList_list dropdown-menu show w-100 overflow-auto" t-att-class="{ 'bottom-100': !composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition }">
<t t-foreach="composerSuggestionListView.composerViewOwner.mainSuggestions" t-as="suggestion" t-key="suggestion.localId">
<ComposerSuggestion record="suggestion"/>
<div class="o_ComposerSuggestionListView position-absolute w-100" t-att-class="{ 'o-lowPosition bottom-0': composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition }" t-attf-class="{{ className }}" t-ref="root">
<div class="o_ComposerSuggestionListView_drop w-100" t-att-class="{ 'dropdown': composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition, 'dropup': !composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition }">
<div class="o_ComposerSuggestionListView_list dropdown-menu show w-100 overflow-auto" t-att-class="{ 'bottom-100': !composerSuggestionListView.composerViewOwner.hasMentionSuggestionsBelowPosition }">
<t t-foreach="composerSuggestionListView.composerViewOwner.mainSuggestionViews" t-as="composerSuggestionView" t-key="composerSuggestionView.localId">
<ComposerSuggestionView record="composerSuggestionView"/>
</t>
<t t-if="composerSuggestionListView.composerViewOwner.mainSuggestions.length > 0 and composerSuggestionListView.composerViewOwner.extraSuggestions.length > 0">
<t t-if="composerSuggestionListView.composerViewOwner.mainSuggestionViews.length > 0 and composerSuggestionListView.composerViewOwner.extraSuggestionViews.length > 0">
<div role="separator" class="dropdown-divider"/>
</t>
<t t-foreach="composerSuggestionListView.composerViewOwner.extraSuggestions" t-as="suggestion" t-key="suggestion.localId">
<ComposerSuggestion record="suggestion"/>
<t t-foreach="composerSuggestionListView.composerViewOwner.extraSuggestionViews" t-as="composerSuggestionView" t-key="composerSuggestionView.localId">
<ComposerSuggestionView record="composerSuggestionView"/>
</t>
</div>
</div>
@@ -5,7 +5,7 @@ import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ComposerSuggestion extends Component {
export class ComposerSuggestionView extends Component {
/**
* @override
@@ -20,9 +20,9 @@ export class ComposerSuggestion extends Component {
//--------------------------------------------------------------------------
/**
* @returns {ComposerSuggestion}
* @returns {ComposerSuggestionView}
*/
get composerSuggestion() {
get composerSuggestionView() {
return this.props.record;
}
@@ -36,21 +36,21 @@ export class ComposerSuggestion extends Component {
_update() {
if (
this.root.el &&
this.composerSuggestion.composerViewOwner.hasToScrollToActiveSuggestion &&
this.composerSuggestion.isActive
this.composerSuggestionView.composerViewOwner.hasToScrollToActiveSuggestionView &&
this.composerSuggestionView.isActive
) {
this.root.el.scrollIntoView({
block: 'center',
});
this.composerSuggestion.composerViewOwner.update({ hasToScrollToActiveSuggestion: false });
this.composerSuggestionView.composerViewOwner.update({ hasToScrollToActiveSuggestionView: false });
}
}
}
Object.assign(ComposerSuggestion, {
Object.assign(ComposerSuggestionView, {
props: { record: Object },
template: 'mail.ComposerSuggestion',
template: 'mail.ComposerSuggestionView',
});
registerMessagingComponent(ComposerSuggestion);
registerMessagingComponent(ComposerSuggestionView);
@@ -2,6 +2,6 @@
// Layout
// ------------------------------------------------------------------
.o_ComposerSuggestion:active {
.o_ComposerSuggestionView:active {
background-color: $gray-300;
}
@@ -1,30 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ComposerSuggestion" owl="1">
<t t-if="composerSuggestion">
<a class="o_ComposerSuggestion dropdown-item d-flex w-100 py-2 px-4" t-att-class="{ 'active bg-300': composerSuggestion.isActive }" t-attf-class="{{ className }}" href="#" t-att-title="composerSuggestion.title" role="menuitem" t-on-click="composerSuggestion.onClick" t-ref="root">
<t t-if="composerSuggestion.suggestable.cannedResponse">
<strong class="o_ComposerSuggestion_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestion.suggestable.cannedResponse.source"/></strong>
<em class="o_ComposerSuggestion_part2 text-600 text-truncate"><t t-esc="composerSuggestion.suggestable.cannedResponse.substitution"/></em>
<t t-name="mail.ComposerSuggestionView" owl="1">
<t t-if="composerSuggestionView">
<a class="o_ComposerSuggestionView dropdown-item d-flex w-100 py-2 px-4" t-att-class="{ 'active bg-300': composerSuggestionView.isActive }" t-attf-class="{{ className }}" href="#" t-att-title="composerSuggestionView.title" role="menuitem" t-on-click="composerSuggestionView.onClick" t-ref="root">
<t t-if="composerSuggestionView.suggestable.cannedResponse">
<strong class="o_ComposerSuggestionView_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestionView.suggestable.cannedResponse.source"/></strong>
<em class="o_ComposerSuggestionView_part2 text-600 text-truncate"><t t-esc="composerSuggestionView.suggestable.cannedResponse.substitution"/></em>
</t>
<t t-if="composerSuggestion.suggestable.thread">
<strong class="o_ComposerSuggestion_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestion.suggestable.thread.name"/></strong>
<t t-if="composerSuggestionView.suggestable.thread">
<strong class="o_ComposerSuggestionView_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestionView.suggestable.thread.name"/></strong>
</t>
<t t-if="composerSuggestion.suggestable.channelCommand">
<strong class="o_ComposerSuggestion_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestion.suggestable.channelCommand.name"/></strong>
<em class="o_ComposerSuggestion_part2 text-600 text-truncate"><t t-esc="composerSuggestion.suggestable.channelCommand.help"/></em>
<t t-if="composerSuggestionView.suggestable.channelCommand">
<strong class="o_ComposerSuggestionView_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestionView.suggestable.channelCommand.name"/></strong>
<em class="o_ComposerSuggestionView_part2 text-600 text-truncate"><t t-esc="composerSuggestionView.suggestable.channelCommand.help"/></em>
</t>
<t t-if="composerSuggestion.suggestable.partner">
<PersonaImStatusIcon
t-if="composerSuggestion.personaImStatusIconView"
className="'o_ComposerSuggestion_personaImStatusIcon flex-shrink-0 me-1'"
hasBackground="false"
record="composerSuggestion.personaImStatusIconView"
/>
<strong class="o_ComposerSuggestion_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestion.suggestable.partner.nameOrDisplayName"/></strong>
<t t-if="composerSuggestion.suggestable.partner.email">
<em class="o_ComposerSuggestion_part2 text-600 text-truncate">(<t t-esc="composerSuggestion.suggestable.partner.email"/>)</em>
<t t-if="composerSuggestionView.suggestable.partner">
<t t-if="composerSuggestionView.personaImStatusIconView">
<PersonaImStatusIcon
className="'o_ComposerSuggestionView_personaImStatusIcon flex-shrink-0 me-1'"
hasBackground="false"
record="composerSuggestionView.personaImStatusIconView"
/>
</t>
<strong class="o_ComposerSuggestionView_part1 flex-shrink-0 mw-100 pe-2 text-truncate"><t t-esc="composerSuggestionView.suggestable.partner.nameOrDisplayName"/></strong>
<t t-if="composerSuggestionView.suggestable.partner.email">
<em class="o_ComposerSuggestionView_part2 text-600 text-truncate">(<t t-esc="composerSuggestionView.suggestable.partner.email"/>)</em>
</t>
</t>
</a>
@@ -5,7 +5,7 @@
<t t-if="composerView">
<div class="o_ComposerTextInput position-relative bg-white" t-attf-class="{{ className }} {{ composerView.isCompact ? 'border border-end-0' : 'border-0' }}" t-on-paste="composerView.onPasteTextInput" t-ref="root">
<t t-if="composerView.composerSuggestionListView">
<ComposerSuggestionList record="composerView.composerSuggestionListView"/>
<ComposerSuggestionListView record="composerView.composerSuggestionListView"/>
</t>
<textarea class="o_ComposerTextInput_textarea o_ComposerTextInput_textareaStyle form-control px-3 border-0 shadow-none overflow-auto" t-att-class="{ 'o-composer-is-compact rounded-0 rounded-start-3': composerView.isCompact, 'rounded-3': !composerView.isCompact }" t-esc="composerView.composer.textInputContent" t-att-placeholder="composerView.composer.placeholder" t-on-click="composerView.onClickTextarea" t-on-focusin="composerView.onFocusinTextarea" t-on-focusout="composerView.onFocusoutTextarea" t-on-keydown="composerView.onKeydownTextarea" t-on-keyup="composerView.onKeyupTextarea" t-on-input="composerView.onInputTextarea" t-ref="textarea"/>
<!--
@@ -15,7 +15,7 @@ registerModel({
inverse: 'suggestable',
readonly: true,
}),
composerSuggestions: many('ComposerSuggestion', {
composerSuggestionViews: many('ComposerSuggestionView', {
inverse: 'suggestable',
isCausal: true,
}),
@@ -12,7 +12,7 @@ import { sprintf } from '@web/core/utils/strings';
* and display suggested partners to mention.
*/
registerModel({
name: 'ComposerSuggestion',
name: 'ComposerSuggestionView',
identifyingFields: [['composerViewOwnerAsExtraSuggestion', 'composerViewOwnerAsMainSuggestion'], 'suggestable'],
recordMethods: {
/**
@@ -40,10 +40,10 @@ registerModel({
* @returns {boolean|FieldCommand}
*/
_computeIsActive() {
if (this.composerViewOwnerAsMainSuggestion && this === this.composerViewOwnerAsMainSuggestion.activeSuggestion) {
if (this.composerViewOwnerAsMainSuggestion && this === this.composerViewOwnerAsMainSuggestion.activeSuggestionView) {
return true;
}
if (this.composerViewOwnerAsExtraSuggestion && this === this.composerViewOwnerAsExtraSuggestion.activeSuggestion) {
if (this.composerViewOwnerAsExtraSuggestion && this === this.composerViewOwnerAsExtraSuggestion.activeSuggestionView) {
return true;
}
return clear();
@@ -102,11 +102,11 @@ registerModel({
required: true,
}),
composerViewOwnerAsExtraSuggestion: one('ComposerView', {
inverse: 'extraSuggestions',
inverse: 'extraSuggestionViews',
readonly: true,
}),
composerViewOwnerAsMainSuggestion: one('ComposerView', {
inverse: 'mainSuggestions',
inverse: 'mainSuggestionViews',
readonly: true,
}),
isActive: attr({
@@ -126,7 +126,7 @@ registerModel({
readonly: true,
}),
suggestable: one('ComposerSuggestable', {
inverse: 'composerSuggestions',
inverse: 'composerSuggestionViews',
readonly: true,
required: true,
}),
+65 -65
View File
@@ -134,7 +134,7 @@ registerModel({
this.composer.textInputContent.length
);
}
const recordReplacement = this.activeSuggestion.mentionText;
const recordReplacement = this.activeSuggestionView.mentionText;
const updateData = {
textInputContent: textLeft + recordReplacement + ' ' + textRight,
textInputCursorEnd: textLeft.length + recordReplacement.length + 1,
@@ -143,11 +143,11 @@ registerModel({
// Specific cases for channel and partner mentions: the message with
// the mention will appear in the target channel, or be notified to
// the target partner.
if (this.activeSuggestion.suggestable.thread) {
Object.assign(updateData, { mentionedChannels: link(this.activeSuggestion.suggestable.thread) });
if (this.activeSuggestionView.suggestable.thread) {
Object.assign(updateData, { mentionedChannels: link(this.activeSuggestionView.suggestable.thread) });
}
if (this.activeSuggestion.suggestable.partner) {
Object.assign(updateData, { mentionedPartners: link(this.activeSuggestion.suggestable.partner) });
if (this.activeSuggestionView.suggestable.partner) {
Object.assign(updateData, { mentionedPartners: link(this.activeSuggestionView.suggestable.partner) });
}
this.composer.update(updateData);
for (const composerView of this.composer.composerViews) {
@@ -274,10 +274,10 @@ registerModel({
});
},
/**
* @param {ComposerSuggestion} suggestion
* @param {ComposerSuggestionView} suggestionView
*/
onClickSuggestion(suggestion) {
this.update({ activeSuggestion: replace(suggestion) });
onClickSuggestion(suggestionView) {
this.update({ activeSuggestionView: replace(suggestionView) });
this.insertSuggestion();
this.closeSuggestions();
this.update({ doFocus: true });
@@ -448,8 +448,8 @@ registerModel({
break;
}
if (this.hasSuggestions) {
this.setPreviousSuggestionActive();
this.update({ hasToScrollToActiveSuggestion: true });
this.setPreviousSuggestionViewActive();
this.update({ hasToScrollToActiveSuggestionView: true });
}
break;
case 'ArrowDown':
@@ -459,30 +459,30 @@ registerModel({
break;
}
if (this.hasSuggestions) {
this.setNextSuggestionActive();
this.update({ hasToScrollToActiveSuggestion: true });
this.setNextSuggestionViewActive();
this.update({ hasToScrollToActiveSuggestionView: true });
}
break;
case 'Home':
if (this.hasSuggestions) {
this.setFirstSuggestionActive();
this.update({ hasToScrollToActiveSuggestion: true });
this.setFirstSuggestionViewActive();
this.update({ hasToScrollToActiveSuggestionView: true });
}
break;
case 'End':
if (this.hasSuggestions) {
this.setLastSuggestionActive();
this.update({ hasToScrollToActiveSuggestion: true });
this.setLastSuggestionViewActive();
this.update({ hasToScrollToActiveSuggestionView: true });
}
break;
case 'Tab':
if (this.hasSuggestions) {
if (ev.shiftKey) {
this.setPreviousSuggestionActive();
this.update({ hasToScrollToActiveSuggestion: true });
this.setPreviousSuggestionViewActive();
this.update({ hasToScrollToActiveSuggestionView: true });
} else {
this.setNextSuggestionActive();
this.update({ hasToScrollToActiveSuggestion: true });
this.setNextSuggestionViewActive();
this.update({ hasToScrollToActiveSuggestionView: true });
}
}
break;
@@ -682,53 +682,53 @@ registerModel({
* Sets the first suggestion as active. Main and extra records are
* considered together.
*/
setFirstSuggestionActive() {
const suggestions = this.mainSuggestions.concat(this.extraSuggestions);
const firstSuggestion = suggestions[0];
this.update({ activeSuggestion: replace(firstSuggestion) });
setFirstSuggestionViewActive() {
const suggestionViews = this.mainSuggestionViews.concat(this.extraSuggestionViews);
const firstSuggestionView = suggestionViews[0];
this.update({ activeSuggestionView: replace(firstSuggestionView) });
},
/**
* Sets the last suggestion as active. Main and extra records are
* considered together.
*/
setLastSuggestionActive() {
const suggestions = this.mainSuggestions.concat(this.extraSuggestions);
const { length, [length - 1]: lastSuggestion } = suggestions;
this.update({ activeSuggestion: replace(lastSuggestion) });
setLastSuggestionViewActive() {
const suggestionViews = this.mainSuggestionViews.concat(this.extraSuggestionViews);
const { length, [length - 1]: lastSuggestionView } = suggestionViews;
this.update({ activeSuggestionView: replace(lastSuggestionView) });
},
/**
* Sets the next suggestion as active. Main and extra records are
* considered together.
*/
setNextSuggestionActive() {
const suggestions = this.mainSuggestions.concat(this.extraSuggestions);
const activeElementIndex = suggestions.findIndex(
suggestion => suggestion === this.activeSuggestion
setNextSuggestionViewActive() {
const suggestionViews = this.mainSuggestionViews.concat(this.extraSuggestionViews);
const activeElementIndex = suggestionViews.findIndex(
suggestion => suggestion === this.activeSuggestionView
);
if (activeElementIndex === suggestions.length - 1) {
if (activeElementIndex === suggestionViews.length - 1) {
// loop when reaching the end of the list
this.setFirstSuggestionActive();
this.setFirstSuggestionViewActive();
return;
}
const nextSuggestion = suggestions[activeElementIndex + 1];
this.update({ activeSuggestion: replace(nextSuggestion) });
const nextSuggestionView = suggestionViews[activeElementIndex + 1];
this.update({ activeSuggestionView: replace(nextSuggestionView) });
},
/**
* Sets the previous suggestion as active. Main and extra records are
* considered together.
*/
setPreviousSuggestionActive() {
const suggestions = this.mainSuggestions.concat(this.extraSuggestions);
const activeElementIndex = suggestions.findIndex(
suggestion => suggestion === this.activeSuggestion
setPreviousSuggestionViewActive() {
const suggestionViews = this.mainSuggestionViews.concat(this.extraSuggestionViews);
const activeElementIndex = suggestionViews.findIndex(
suggestion => suggestion === this.activeSuggestionView
);
if (activeElementIndex === 0) {
// loop when reaching the start of the list
this.setLastSuggestionActive();
this.setLastSuggestionViewActive();
return;
}
const previousSuggestion = suggestions[activeElementIndex - 1];
this.update({ activeSuggestion: replace(previousSuggestion) });
const previousSuggestionView = suggestionViews[activeElementIndex - 1];
this.update({ activeSuggestionView: replace(previousSuggestionView) });
},
/**
* Updates the textarea height of text input.
@@ -775,22 +775,22 @@ registerModel({
* @private
* @returns {FieldCommand}
*/
_computeActiveSuggestion() {
_computeActiveSuggestionView() {
if (
this.mainSuggestions.length === 0 &&
this.extraSuggestions.length === 0
this.mainSuggestionViews.length === 0 &&
this.extraSuggestionViews.length === 0
) {
return clear();
}
if (
this.mainSuggestions.includes(this.activeSuggestion) ||
this.extraSuggestions.includes(this.activeSuggestion)
this.mainSuggestionViews.includes(this.activeSuggestionView) ||
this.extraSuggestionViews.includes(this.activeSuggestionView)
) {
return;
}
const suggestions = this.mainSuggestions.concat(this.extraSuggestions);
const firstSuggestion = suggestions[0];
return replace(firstSuggestion);
const suggestionViews = this.mainSuggestionViews.concat(this.extraSuggestionViews);
const firstSuggestionView = suggestionViews[0];
return replace(firstSuggestionView);
},
/**
* @private
@@ -884,11 +884,11 @@ registerModel({
* @private
* @returns {FieldCommand}
*/
_computeExtraSuggestions() {
_computeExtraSuggestionViews() {
if (this.suggestionDelimiterPosition === undefined) {
return clear();
}
return unlink(this.mainSuggestions);
return unlink(this.mainSuggestionViews);
},
/**
* @private
@@ -960,7 +960,7 @@ registerModel({
* @return {boolean}
*/
_computeHasSuggestions() {
return this.mainSuggestions.length > 0 || this.extraSuggestions.length > 0;
return this.mainSuggestionViews.length > 0 || this.extraSuggestionViews.length > 0;
},
/**
* @private
@@ -1024,7 +1024,7 @@ registerModel({
* @private
* @returns {Record[]}
*/
_computeMainSuggestions() {
_computeMainSuggestionViews() {
if (this.suggestionDelimiterPosition === undefined) {
return clear();
}
@@ -1389,15 +1389,15 @@ registerModel({
mainSuggestedRecords.length = Math.min(mainSuggestedRecords.length, limit);
extraSuggestedRecords.length = Math.min(extraSuggestedRecords.length, limit - mainSuggestedRecords.length);
this.update({
extraSuggestions: insertAndReplace(
extraSuggestionViews: insertAndReplace(
extraSuggestedRecords.map(record => {
return {
suggestable: replace(record.suggestable),
};
}),
),
hasToScrollToActiveSuggestion: true,
mainSuggestions: insertAndReplace(
hasToScrollToActiveSuggestionView: true,
mainSuggestionViews: insertAndReplace(
mainSuggestedRecords.map(record => {
return {
suggestable: replace(record.suggestable),
@@ -1423,8 +1423,8 @@ registerModel({
* is highlighted in the UI and it will be selected when the
* suggestion is confirmed by the user.
*/
activeSuggestion: one('ComposerSuggestion', {
compute: '_computeActiveSuggestion',
activeSuggestionView: one('ComposerSuggestionView', {
compute: '_computeActiveSuggestionView',
}),
/**
* Determines the attachment list that will be used to display the attachments.
@@ -1493,8 +1493,8 @@ registerModel({
/**
* Determines the extra suggestions.
*/
extraSuggestions: many('ComposerSuggestion', {
compute: '_computeExtraSuggestions',
extraSuggestionViews: many('ComposerSuggestionView', {
compute: '_computeExtraSuggestionViews',
inverse: 'composerViewOwnerAsExtraSuggestion',
isCausal: true,
}),
@@ -1574,7 +1574,7 @@ registerModel({
* Determines whether the currently active suggestion should be scrolled
* into view.
*/
hasToScrollToActiveSuggestion: attr({
hasToScrollToActiveSuggestionView: attr({
default: false,
}),
isCompact: attr({
@@ -1598,8 +1598,8 @@ registerModel({
/**
* Determines the main suggestions.
*/
mainSuggestions: many('ComposerSuggestion', {
compute: '_computeMainSuggestions',
mainSuggestionViews: many('ComposerSuggestionView', {
compute: '_computeMainSuggestionViews',
inverse: 'composerViewOwnerAsMainSuggestion',
isCausal: true,
}),
@@ -54,7 +54,7 @@ registerModel({
inverse: 'personaImStatusIconView',
readonly: true,
}),
composerSuggestionViewOwner: one('ComposerSuggestion', {
composerSuggestionViewOwner: one('ComposerSuggestionView', {
inverse: 'personaImStatusIconView',
readonly: true,
}),
@@ -387,14 +387,14 @@ function getCreateComposerComponent({ env, target }) {
};
}
function getCreateComposerSuggestionComponent({ env, target }) {
return async function createComposerSuggestionComponent(composer, props) {
function getCreateComposerSuggestionViewComponent({ env, target }) {
return async function createComposerSuggestionViewComponent(composer, props) {
const composerView = env.services.messaging.modelManager.messaging.models['ComposerView'].create({
qunitTest: insertAndReplace({
composer: replace(composer),
}),
});
await createRootMessagingComponent(env, "ComposerSuggestion", {
await createRootMessagingComponent(env, "ComposerSuggestionView", {
props: { ...props, composerView: composerView },
target,
});
@@ -573,7 +573,7 @@ async function start(param0 = {}) {
afterNextRender,
click: getClick({ afterNextRender }),
createComposerComponent: getCreateComposerComponent({ env: webClient.env, target }),
createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ env: webClient.env, target }),
createComposerSuggestionViewComponent: getCreateComposerSuggestionViewComponent({ env: webClient.env, target }),
createNotificationListComponent: getCreateNotificationListComponent({ env: webClient.env, target }),
createRootMessagingComponent: (componentName, props) => createRootMessagingComponent(webClient.env, componentName, { props, target }),
env: webClient.env,
@@ -757,7 +757,7 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "@");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionList_list'),
document.querySelector('.o_ComposerSuggestionListView_list'),
'show',
"should display mention suggestions on typing '@'"
);
@@ -768,7 +768,7 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) {
});
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"mention suggestion should be closed after pressing escape on mention suggestion"
);
assert.containsOnce(
@@ -26,7 +26,7 @@ QUnit.test('canned response suggestion displayed', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', ":hello");
assert.containsOnce(
document.body,
`.o_ComposerSuggestion`,
`.o_ComposerSuggestionView`,
"Canned response suggestion should be present"
);
});
@@ -51,21 +51,21 @@ QUnit.test('canned response suggestion correct data', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', ":hello");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion_part1',
'.o_ComposerSuggestionView_part1',
"Canned response source should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestion_part1`).textContent,
document.querySelector(`.o_ComposerSuggestionView_part1`).textContent,
"hello",
"Canned response source should be displayed"
);
assert.containsOnce(
document.body,
'.o_ComposerSuggestion_part2',
'.o_ComposerSuggestionView_part2',
"Canned response substitution should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestion_part2`).textContent,
document.querySelector(`.o_ComposerSuggestionView_part2`).textContent,
"Hello, how are you?",
"Canned response substitution should be displayed"
);
@@ -90,7 +90,7 @@ QUnit.test('canned response suggestion active', async function (assert) {
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', ":hello");
assert.hasClass(
document.querySelector('.o_ComposerSuggestion'),
document.querySelector('.o_ComposerSuggestionView'),
'active',
"should be active initially"
);
@@ -22,7 +22,7 @@ QUnit.test('channel mention suggestion displayed', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "#my-channel");
assert.containsOnce(
document.body,
`.o_ComposerSuggestion`,
`.o_ComposerSuggestionView`,
"Channel mention suggestion should be present"
);
});
@@ -43,11 +43,11 @@ QUnit.test('channel mention suggestion correct data', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "#General");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion_part1',
'.o_ComposerSuggestionView_part1',
"Channel name should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestion_part1`).textContent,
document.querySelector(`.o_ComposerSuggestionView_part1`).textContent,
"General",
"Channel name should be displayed"
);
@@ -68,7 +68,7 @@ QUnit.test('channel mention suggestion active', async function (assert) {
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#my-channel");
assert.hasClass(
document.querySelector('.o_ComposerSuggestion'),
document.querySelector('.o_ComposerSuggestionView'),
'active',
"should be active initially"
);
@@ -22,7 +22,7 @@ QUnit.test('command suggestion displayed', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "/who");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"Command suggestion should be present",
);
});
@@ -43,21 +43,21 @@ QUnit.test('command suggestion correct data', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "/who");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion_part1',
'.o_ComposerSuggestionView_part1',
"Command name should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestion_part1`).textContent,
document.querySelector(`.o_ComposerSuggestionView_part1`).textContent,
"who",
"Command name should be displayed"
);
assert.containsOnce(
document.querySelector('.o_ComposerSuggestion'),
'.o_ComposerSuggestion_part2',
document.querySelector('.o_ComposerSuggestionView'),
'.o_ComposerSuggestionView_part2',
"Command help should be present"
);
assert.strictEqual(
document.querySelector(`.o_ComposerSuggestion_part2`).textContent,
document.querySelector(`.o_ComposerSuggestionView_part2`).textContent,
"List users in the current channel",
"Command help should be displayed"
);
@@ -78,7 +78,7 @@ QUnit.test('command suggestion active', async function (assert) {
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "/who");
assert.hasClass(
document.querySelector('.o_ComposerSuggestion'),
document.querySelector('.o_ComposerSuggestionView'),
'active',
"1st suggestion should be active initially"
);
@@ -27,7 +27,7 @@ QUnit.test('partner mention suggestion displayed', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "@demo");
assert.containsOnce(
document.body,
`.o_ComposerSuggestion`,
`.o_ComposerSuggestionView`,
"Partner mention suggestion should be present"
);
});
@@ -52,27 +52,27 @@ QUnit.test('partner mention suggestion correct data', async function (assert) {
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "@demo");
assert.containsOnce(
document.querySelector('.o_ComposerSuggestion'),
document.querySelector('.o_ComposerSuggestionView'),
'.o_PersonaImStatusIcon',
"Partner's im_status should be displayed"
);
assert.containsOnce(
document.body,
'.o_ComposerSuggestion_part1',
'.o_ComposerSuggestionView_part1',
"Partner's name should be present"
);
assert.strictEqual(
document.querySelector('.o_ComposerSuggestion_part1').textContent,
document.querySelector('.o_ComposerSuggestionView_part1').textContent,
"Demo User",
"Partner's name should be displayed"
);
assert.containsOnce(
document.body,
'.o_ComposerSuggestion_part2',
'.o_ComposerSuggestionView_part2',
"Partner's email should be present"
);
assert.strictEqual(
document.querySelector('.o_ComposerSuggestion_part2').textContent,
document.querySelector('.o_ComposerSuggestionView_part2').textContent,
"(demo_user@odoo.com)",
"Partner's email should be displayed"
);
@@ -98,7 +98,7 @@ QUnit.test('partner mention suggestion active', async function (assert) {
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "@demo");
assert.hasClass(
document.querySelector('.o_ComposerSuggestion'),
document.querySelector('.o_ComposerSuggestionView'),
'active',
"should be active initially"
);
@@ -261,12 +261,12 @@ QUnit.test('display canned response suggestions on typing ":"', async function (
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"Canned responses suggestions list should not be present"
);
await insertText('.o_ComposerTextInput_textarea', ':');
assert.hasClass(
document.querySelector('.o_ComposerSuggestionList_list'),
document.querySelector('.o_ComposerSuggestionListView_list'),
'show',
"should display canned response suggestions on typing ':'"
);
@@ -287,7 +287,7 @@ QUnit.test('use a canned response', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"canned response suggestions list should not be present"
);
assert.strictEqual(
@@ -298,10 +298,10 @@ QUnit.test('use a canned response', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', ':');
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a canned response suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"Hello! How are you? ",
@@ -324,7 +324,7 @@ QUnit.test('use a canned response some text', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"canned response suggestions list should not be present"
);
assert.strictEqual(
@@ -341,10 +341,10 @@ QUnit.test('use a canned response some text', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', ':');
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a canned response suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"bluhbluh Hello! How are you? ",
@@ -367,7 +367,7 @@ QUnit.test('add an emoji after a canned response', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"canned response suggestions list should not be present"
);
assert.strictEqual(
@@ -378,10 +378,10 @@ QUnit.test('add an emoji after a canned response', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', ':');
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a canned response suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"Hello! How are you? ",
@@ -412,12 +412,12 @@ QUnit.test('display channel mention suggestions on typing "#"', async function (
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"channel mention suggestions list should not be present"
);
await insertText('.o_ComposerTextInput_textarea', "#");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionList_list'),
document.querySelector('.o_ComposerSuggestionListView_list'),
'show',
"should display channel mention suggestions on typing '#'"
);
@@ -437,7 +437,7 @@ QUnit.test('mention a channel', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"channel mention suggestions list should not be present"
);
assert.strictEqual(
@@ -448,10 +448,10 @@ QUnit.test('mention a channel', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "#");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a channel mention suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"#General ",
@@ -473,7 +473,7 @@ QUnit.test('mention a channel after some text', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"channel mention suggestions list should not be present"
);
assert.strictEqual(
@@ -490,10 +490,10 @@ QUnit.test('mention a channel after some text', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "#");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a channel mention suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"bluhbluh #General ",
@@ -515,7 +515,7 @@ QUnit.test('add an emoji after a channel mention', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"mention suggestions list should not be present"
);
assert.strictEqual(
@@ -526,10 +526,10 @@ QUnit.test('add an emoji after a channel mention', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "#");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a channel mention suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"#General ",
@@ -560,12 +560,12 @@ QUnit.test('display command suggestions on typing "/"', async function (assert)
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"command suggestions list should not be present"
);
await insertText('.o_ComposerTextInput_textarea', "/");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionList_list'),
document.querySelector('.o_ComposerSuggestionListView_list'),
'show',
"should display command suggestions on typing '/'"
);
@@ -614,7 +614,7 @@ QUnit.test('do not send typing notification on typing after selecting suggestion
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "/");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
await insertText('.o_ComposerTextInput_textarea', " is user?");
assert.verifySteps([], "No rpc done");
});
@@ -633,7 +633,7 @@ QUnit.test('use a command for a specific channel type', async function (assert)
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"command suggestions list should not be present"
);
assert.strictEqual(
@@ -642,7 +642,7 @@ QUnit.test('use a command for a specific channel type', async function (assert)
"text content of composer should be empty initially"
);
await insertText('.o_ComposerTextInput_textarea', "/");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"/who ",
@@ -663,7 +663,7 @@ QUnit.test('command suggestion should only open if command is the first characte
await createComposerComponent(thread.composer);
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"command suggestions list should not be present"
);
assert.strictEqual(
@@ -680,7 +680,7 @@ QUnit.test('command suggestion should only open if command is the first characte
await insertText('.o_ComposerTextInput_textarea', "/");
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should not have a command suggestion"
);
});
@@ -699,7 +699,7 @@ QUnit.test('add an emoji after a command', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"command suggestions list should not be present"
);
assert.strictEqual(
@@ -708,7 +708,7 @@ QUnit.test('add an emoji after a command', async function (assert) {
"text content of composer should be empty initially"
);
await insertText('.o_ComposerTextInput_textarea', "/");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"/who ",
@@ -743,12 +743,12 @@ QUnit.test('display partner mention suggestions on typing "@"', async function (
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"mention suggestions list should not be present"
);
await insertText('.o_ComposerTextInput_textarea', "@");
assert.hasClass(
document.querySelector('.o_ComposerSuggestionList_list'),
document.querySelector('.o_ComposerSuggestionListView_list'),
'show',
"should display mention suggestions on typing '@'"
);
@@ -774,7 +774,7 @@ QUnit.test('mention a partner', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestionList_list',
'.o_ComposerSuggestionListView_list',
"mention suggestions list should not be present"
);
assert.strictEqual(
@@ -785,10 +785,10 @@ QUnit.test('mention a partner', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', '@Te');
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a mention suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"@TestPartner ",
@@ -811,7 +811,7 @@ QUnit.test('mention a partner after some text', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"mention suggestions list should not be present"
);
assert.strictEqual(
@@ -828,10 +828,10 @@ QUnit.test('mention a partner after some text', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "@Te");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a mention suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"bluhbluh @TestPartner ",
@@ -854,7 +854,7 @@ QUnit.test('add an emoji after a partner mention', async function (assert) {
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"mention suggestions list should not be present"
);
assert.strictEqual(
@@ -865,10 +865,10 @@ QUnit.test('add an emoji after a partner mention', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "@Te");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"should have a mention suggestion"
);
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"@TestPartner ",
@@ -86,7 +86,7 @@ QUnit.test('reply: discard on pressing escape', async function (assert) {
await insertText('.o_ComposerTextInput_textarea', "@Te");
assert.containsOnce(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"mention suggestion should be opened after typing @"
);
@@ -96,7 +96,7 @@ QUnit.test('reply: discard on pressing escape', async function (assert) {
});
assert.containsNone(
document.body,
'.o_ComposerSuggestion',
'.o_ComposerSuggestionView',
"mention suggestion should be closed after pressing escape on mention suggestion"
);
assert.containsOnce(
@@ -878,7 +878,7 @@ QUnit.test('Post a message containing an email address followed by a mention on
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "email@odoo.com\n");
await insertText('.o_ComposerTextInput_textarea', "@Te");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
await click('.o_Composer_buttonSend');
assert.containsOnce(
document.querySelector(`.o_Message_content`),
@@ -903,7 +903,7 @@ QUnit.test(`Mention a partner with special character (e.g. apostrophe ')`, async
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "@Pyn");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
await click('.o_Composer_buttonSend');
assert.containsOnce(
document.querySelector(`.o_Message_content`),
@@ -934,9 +934,9 @@ QUnit.test('mention 2 different partners that have the same name', async functio
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "@Te");
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[0].click());
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[0].click());
await insertText('.o_ComposerTextInput_textarea', "@Te");
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[1].click());
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[1].click());
await click('.o_Composer_buttonSend');
assert.containsOnce(document.body, '.o_Message_content', 'should have one message after posting it');
assert.containsOnce(
@@ -965,7 +965,7 @@ QUnit.test('mention a channel with space in the name', async function (assert) {
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
await click('.o_Composer_buttonSend');
assert.containsOnce(
document.querySelector('.o_Message_content'),
@@ -994,7 +994,7 @@ QUnit.test('mention a channel with "&" in the name', async function (assert) {
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
await click('.o_Composer_buttonSend');
assert.containsOnce(
document.querySelector('.o_Message_content'),
@@ -1023,7 +1023,7 @@ QUnit.test('mention a channel on a second line when the first line contains #',
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#blabla\n");
await insertText('.o_ComposerTextInput_textarea', "#");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
await click('.o_Composer_buttonSend');
assert.containsOnce(
document.querySelector('.o_Message_content'),
@@ -1052,7 +1052,7 @@ QUnit.test('mention a channel when replacing the space after the mention by anot
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "#");
await click('.o_ComposerSuggestion');
await click('.o_ComposerSuggestionView');
const text = document.querySelector(`.o_ComposerTextInput_textarea`).value;
document.querySelector(`.o_ComposerTextInput_textarea`).value = text.slice(0, -1);
await insertText('.o_ComposerTextInput_textarea', ", test");
@@ -1090,9 +1090,9 @@ QUnit.test('mention 2 different channels that have the same name', async functio
await openDiscuss();
document.querySelector('.o_ComposerTextInput_textarea').focus();
await insertText('.o_ComposerTextInput_textarea', "#my");
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[0].click());
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[0].click());
await insertText('.o_ComposerTextInput_textarea', "#my");
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[1].click());
await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[1].click());
await click('.o_Composer_buttonSend');
assert.containsOnce(document.body, '.o_Message_content', 'should have one message after posting it');
assert.containsOnce(