[FIX] mail: debounce suggestion (and channel selector) searches

Before this commit, fetching suggestions (e.g. channel selector
in discuss sidebar "Find or create a channel", or `@`-mentions)
were performing RPCs on each user input, resulting in wasted
network and server load.

This commit fixes the issue by debouncing the fetching of
suggestions to 0.5 sec.

closes odoo/odoo#126952

X-original-commit: 6770ac3
Related: odoo/enterprise#43502
Signed-off-by: Matthieu Stockbauer (tsm) <tsm@odoo.com>
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2023-07-04 03:43:48 +02:00
parent 9c338783af
commit 987eeef710
17 changed files with 239 additions and 67 deletions
@@ -9,6 +9,7 @@ import {
insertText,
start,
} from "@mail/../tests/helpers/test_utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { editInput, nextTick, triggerHotkey } from "@web/../tests/helpers/utils";
@@ -111,10 +112,13 @@ QUnit.test('display canned response suggestions on typing ":"', async (assert) =
source: "hello",
substitution: "Hello! How are you?",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
await insertText(".o-mail-Composer-input", ":");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open");
});
@@ -133,11 +137,14 @@ QUnit.test("use a canned response", async (assert) => {
source: "hello",
substitution: "Hello! How are you?",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", ":");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
@@ -162,13 +169,16 @@ QUnit.test("use a canned response some text", async (assert) => {
source: "hello",
substitution: "Hello! How are you?",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "bluhbluh ");
assert.strictEqual($(".o-mail-Composer-input").val(), "bluhbluh ");
await insertText(".o-mail-Composer-input", ":");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
@@ -193,11 +203,14 @@ QUnit.test("add an emoji after a canned response", async (assert) => {
source: "hello",
substitution: "Hello! How are you?",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", ":");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
@@ -65,9 +65,6 @@ export class NavigableList extends Component {
}
open() {
if (this.state.isLoading) {
return;
}
this.load().then(() => {
this.state.open = true;
this.navigate("first");
@@ -80,6 +77,7 @@ export class NavigableList extends Component {
}
async load() {
this.state.isLoading = true;
this.state.options = [];
const makeOption = (opt) => {
return Object.assign(Object.create(opt), {
@@ -87,12 +85,12 @@ export class NavigableList extends Component {
});
};
if (this.props.options instanceof Promise) {
this.state.isLoading = true;
return this.props.options.then((opts) => {
opts.forEach((opt) => this.state.options.push(makeOption(opt)));
this.state.isLoading = false;
});
}
this.state.isLoading = false;
if (this.props.options instanceof Array) {
if (this.props.options.length === 0) {
return;
@@ -1,8 +1,10 @@
/* @odoo-module */
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { useComponent, useEffect, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { useDebounced } from "@web/core/utils/timing";
export function useSuggestion() {
const comp = useComponent();
@@ -124,9 +126,10 @@ export function useSuggestion() {
state: useState({
count: 0,
items: undefined,
loading: false,
}),
update() {
if (!self.search.delimiter) {
if (!self.search.delimiter || self.state.loading) {
return;
}
const suggestions = suggestionService.searchSuggestions(
@@ -150,6 +153,10 @@ export function useSuggestion() {
self.state.items = { type, mainSuggestions, extraSuggestions };
},
};
const debouncedFetchSuggestions = useDebounced(
(...args) => suggestionService.fetchSuggestions(...args),
DEBOUNCE_FETCH_SUGGESTION_TIME
);
useEffect(
() => {
self.update();
@@ -157,12 +164,14 @@ export function useSuggestion() {
if (self.search.position === undefined || !self.search.delimiter) {
return; // ignore obsolete call
}
await suggestionService.fetchSuggestions(self.search, {
self.state.loading = true;
await debouncedFetchSuggestions(self.search, {
thread: self.thread,
onFetched() {
if (owl.status(comp) === "destroyed") {
return;
}
self.state.loading = false;
self.update();
},
});
@@ -4,6 +4,8 @@ import { cleanTerm } from "@mail/utils/common/format";
import { registry } from "@web/core/registry";
export const DEBOUNCE_FETCH_SUGGESTION_TIME = 500;
export class SuggestionService {
constructor(env, services) {
this.orm = services.orm;
@@ -2,6 +2,7 @@
import { useStore } from "@mail/core/common/messaging_hook";
import { NavigableList } from "@mail/core/common/navigable_list";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { cleanTerm } from "@mail/utils/common/format";
import { createLocalId, isEventHandled, markEventHandled } from "@mail/utils/common/misc";
@@ -11,6 +12,7 @@ import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { _t } from "@web/core/l10n/translation";
import { TagsList } from "@web/core/tags_list/tags_list";
import { useService } from "@web/core/utils/hooks";
import { useDebounced } from "@web/core/utils/timing";
export class ChannelSelector extends Component {
static components = { TagsList, NavigableList };
@@ -35,6 +37,10 @@ export class ChannelSelector extends Component {
onMounted(() => this.inputRef.el.focus());
}
this.markEventHandled = markEventHandled;
this.debouncedFetchSuggestions = useDebounced(
this.fetchSuggestions.bind(this),
DEBOUNCE_FETCH_SUGGESTION_TIME
);
}
async fetchSuggestions() {
@@ -198,7 +204,7 @@ export class ChannelSelector extends Component {
this.props.category.id === "channels"
? "discuss.ChannelSelector.channel"
: "discuss.ChannelSelector.chat",
options: this.fetchSuggestions(),
options: this.debouncedFetchSuggestions(),
};
}
}
@@ -28,6 +28,7 @@ import {
import { file } from "web.test_utils";
const { inputFiles } = file;
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
QUnit.module("composer", {
async beforeEach() {
@@ -359,13 +360,16 @@ QUnit.test("add an emoji after a partner mention", async (assert) => {
Command.create({ partner_id: partnerId }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "T");
await insertText(".o-mail-Composer-input", "e");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val().replace(/\s/, " "), "@TestPartner ");
@@ -380,7 +384,7 @@ QUnit.test("mention a channel after some text", async (assert) => {
name: "General",
channel_type: "channel",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
@@ -391,6 +395,9 @@ QUnit.test("mention a channel after some text", async (assert) => {
"text content of composer should have content"
);
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
@@ -406,11 +413,14 @@ QUnit.test("add an emoji after a channel mention", async (assert) => {
name: "General",
channel_type: "channel",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestion");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
@@ -427,10 +437,13 @@ QUnit.test("add an emoji after a channel mention", async (assert) => {
QUnit.test("pending mentions are kept when toggling composer", async (assert) => {
const pyEnv = await startServer();
const { openFormView } = await start();
const { advanceTime, openFormView } = await start({ hasTimeControl: true });
await openFormView("res.partner", pyEnv.currentPartnerId);
await click("button:contains(Send message)");
await insertText(".o-mail-Composer-input", "@");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:contains(Mitchell Admin)");
await click("button:contains(Send message)");
await click("button:contains(Send message)");
@@ -611,7 +624,8 @@ QUnit.test("Select composer suggestion via Enter does not send the message", asy
Command.create({ partner_id: partnerId }),
],
});
const { openDiscuss } = await start({
const { advanceTime, openDiscuss } = await start({
hasTimeControl: true,
async mockRPC(route, args) {
if (route === "/mail/message/post") {
assert.step("message_post");
@@ -621,6 +635,9 @@ QUnit.test("Select composer suggestion via Enter does not send the message", asy
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Shrek");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await afterNextRender(() => triggerHotkey("Enter"));
assert.equal($(".o-mail-Composer-input").val().trim(), "@Shrek");
assert.verifySteps([]);
@@ -1,8 +1,9 @@
/* @odoo-module */
import { afterNextRender, click, start, startServer } from "@mail/../tests/helpers/test_utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { editInput, triggerEvent } from "@web/../tests/helpers/utils";
import { editInput, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
QUnit.module("call");
@@ -10,7 +11,7 @@ QUnit.test("no default rtc after joining a chat conversation", async (assert) =>
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
pyEnv["res.users"].create({ partner_id: partnerId });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
assert.containsNone($, ".o-mail-DiscussCategoryItem");
@@ -18,6 +19,8 @@ QUnit.test("no default rtc after joining a chat conversation", async (assert) =>
await afterNextRender(() =>
editInput(document.body, ".o-discuss-ChannelSelector input", "mario")
);
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
await triggerEvent(document.body, ".o-discuss-ChannelSelector input", "keydown", {
key: "Enter",
@@ -34,17 +37,21 @@ QUnit.test("no default rtc after joining a group conversation", async (assert) =
{ name: "Luigi" },
]);
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
assert.containsNone($, ".o-mail-DiscussCategoryItem");
await click(".o-mail-DiscussSidebar i[title='Start a conversation']");
await afterNextRender(() =>
editInput(document.body, ".o-discuss-ChannelSelector input", "mario")
);
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
await afterNextRender(() =>
editInput(document.body, ".o-discuss-ChannelSelector input", "luigi")
);
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
await triggerEvent(document.body, ".o-discuss-ChannelSelector input", "keydown", {
key: "Enter",
@@ -4,7 +4,8 @@ import { Composer } from "@mail/core/common/composer";
import { Command } from "@mail/../tests/helpers/command";
import { click, insertText, start, startServer } from "@mail/../tests/helpers/test_utils";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
QUnit.module("suggestion", {
async beforeEach() {
@@ -110,7 +111,7 @@ QUnit.test("Sort partner suggestions by recent chats", async (assert) => {
channel_type: "chat",
},
]);
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
await click(".o-mail-DiscussCategoryItem:contains('User 2')");
await insertText(".o-mail-Composer-input", "This is a test");
@@ -119,6 +120,9 @@ QUnit.test("Sort partner suggestions by recent chats", async (assert) => {
await click(".o-mail-DiscussCategoryItem:contains('General')");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "User");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsN($, ".o-mail-Composer-suggestion", 3);
assert.strictEqual($(".o-mail-Composer-suggestion").eq(0).text(), "User 2");
assert.strictEqual($(".o-mail-Composer-suggestion").eq(1).text(), "User 1");
@@ -15,7 +15,8 @@ import {
startServer,
} from "@mail/../tests/helpers/test_utils";
import { makeDeferred } from "@web/../tests/helpers/utils";
import { makeDeferred, nextTick } from "@web/../tests/helpers/utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
QUnit.module("chat window: new message");
@@ -103,7 +104,8 @@ QUnit.test(
1920,
"should have enough space to open 3 chat windows simultaneously"
);
await start({
const { advanceTime } = await start({
hasTimeControl: true,
mockRPC(route, args) {
if (args.method === "im_search") {
imSearchDef.resolve();
@@ -137,6 +139,8 @@ QUnit.test(
// search for a user in "new message" autocomplete
await afterNextRender(async () => {
await insertText(".o-discuss-ChannelSelector input", "131");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await imSearchDef;
});
assert.containsOnce($, ".o-discuss-ChannelSelector-suggestion a");
@@ -171,10 +175,12 @@ QUnit.test(
channel_type: "chat",
name: "Partner 131",
});
await start();
const { advanceTime } = await start({ hasTimeControl: true });
await click(".o_menu_systray i[aria-label='Messages']");
await click("button:contains(New Message)");
await insertText(".o-discuss-ChannelSelector", "131");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion a");
assert.containsNone($, ".o-mail-ChatWindow-header:contains(New message)");
assert.containsOnce($, ".o-mail-ChatWindow");
@@ -186,7 +192,8 @@ QUnit.test("new message autocomplete should automatically select first result",
const partnerId = pyEnv["res.partner"].create({ name: "Partner 131" });
pyEnv["res.users"].create({ partner_id: partnerId });
const imSearchDef = makeDeferred();
await start({
const { advanceTime } = await start({
hasTimeControl: true,
mockRPC(route, args) {
if (args.method === "im_search") {
imSearchDef.resolve();
@@ -199,6 +206,8 @@ QUnit.test("new message autocomplete should automatically select first result",
// search for a user in "new message" autocomplete
await afterNextRender(async () => {
await insertText(".o-discuss-ChannelSelector", "131");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await imSearchDef;
});
assert.hasClass($(".o-discuss-ChannelSelector-suggestion a"), "o-mail-NavigableList-active");
@@ -7,15 +7,16 @@ import {
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { triggerHotkey, patchWithCleanup } from "@web/../tests/helpers/utils";
import { triggerHotkey, patchWithCleanup, nextTick } from "@web/../tests/helpers/utils";
QUnit.module("crosstab");
QUnit.test("Channel subscription is renewed when channel is manually added", async (assert) => {
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({ name: "General", channel_member_ids: [] });
const { env, openDiscuss } = await start();
const { advanceTime, env, openDiscuss } = await start({ hasTimeControl: true });
patchWithCleanup(env.services["bus_service"], {
forceUpdateChannels() {
assert.step("update-channels");
@@ -24,6 +25,8 @@ QUnit.test("Channel subscription is renewed when channel is manually added", asy
await openDiscuss();
await click("[title='Add or join a channel']");
await insertText(".o-discuss-ChannelSelector", "General");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await afterNextRender(() => triggerHotkey("Enter"));
assert.verifySteps(["update-channels"]);
});
@@ -7,14 +7,16 @@ import {
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { editInput, triggerEvent, triggerHotkey } from "@web/../tests/helpers/utils";
import { editInput, nextTick, triggerEvent, triggerHotkey } from "@web/../tests/helpers/utils";
QUnit.module("discuss");
QUnit.test("can create a new channel [REQUIRE FOCUS]", async (assert) => {
await startServer();
const { openDiscuss } = await start({
const { advanceTime, openDiscuss } = await start({
hasTimeControl: true,
mockRPC(route, params) {
if (
route.startsWith("/mail") ||
@@ -35,6 +37,8 @@ QUnit.test("can create a new channel [REQUIRE FOCUS]", async (assert) => {
await afterNextRender(() =>
editInput(document.body, ".o-discuss-ChannelSelector input", "abc")
);
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
assert.containsOnce($, ".o-mail-DiscussCategoryItem");
assert.containsNone($, ".o-mail-Discuss-content .o-mail-Message");
@@ -54,14 +58,15 @@ QUnit.test(
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
pyEnv["res.users"].create({ partner_id: partnerId });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
assert.containsNone($, ".o-mail-DiscussCategoryItem");
await click("i[title='Start a conversation']");
await afterNextRender(() =>
editInput(document.body, ".o-discuss-ChannelSelector input", "mario")
);
await afterNextRender(async () => {
editInput(document.body, ".o-discuss-ChannelSelector input", "mario");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
});
await click(".o-discuss-ChannelSelector-suggestion");
assert.containsOnce($, ".o-discuss-ChannelSelector span[title='Mario']");
assert.containsNone($, ".o-mail-DiscussCategoryItem");
@@ -71,9 +76,10 @@ QUnit.test(
});
assert.containsNone($, ".o-discuss-ChannelSelector span[title='Mario']");
await afterNextRender(() =>
editInput(document.body, ".o-discuss-ChannelSelector input", "mario")
);
await afterNextRender(async () => {
editInput(document.body, ".o-discuss-ChannelSelector input", "mario");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
});
await triggerEvent(document.body, ".o-discuss-ChannelSelector input", "keydown", {
key: "Enter",
});
@@ -86,7 +92,8 @@ QUnit.test("can join a chat conversation", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
pyEnv["res.users"].create({ partner_id: partnerId });
const { openDiscuss } = await start({
const { advanceTime, openDiscuss } = await start({
hasTimeControl: true,
mockRPC(route, params) {
if (
route.startsWith("/mail") ||
@@ -107,6 +114,8 @@ QUnit.test("can join a chat conversation", async (assert) => {
await afterNextRender(() =>
editInput(document.body, ".o-discuss-ChannelSelector input", "mario")
);
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
await triggerEvent(document.body, ".o-discuss-ChannelSelector input", "keydown", {
key: "Enter",
@@ -129,13 +138,17 @@ QUnit.test("can create a group chat conversation", async (assert) => {
{ name: "Luigi" },
]);
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
assert.containsNone($, ".o-mail-DiscussCategoryItem");
await click(".o-mail-DiscussSidebar i[title='Start a conversation']");
await insertText(".o-discuss-ChannelSelector input", "Mario");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
await insertText(".o-discuss-ChannelSelector input", "Luigi");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
await triggerEvent(document.body, ".o-discuss-ChannelSelector input", "keydown", {
key: "Enter",
@@ -145,10 +158,12 @@ QUnit.test("can create a group chat conversation", async (assert) => {
});
QUnit.test("chat search should display no result when no matches found", async (assert) => {
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
await click(".o-mail-DiscussSidebar i[title='Start a conversation']");
await insertText(".o-discuss-ChannelSelector", "Rainbow Panda");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
assert.containsOnce($, ".o-discuss-ChannelSelector-suggestion:contains(No results found)");
});
@@ -158,11 +173,13 @@ QUnit.test(
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Panda" });
pyEnv["res.users"].create({ partner_id: partnerId });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
assert.containsNone($, ".o-mail-DiscussCategoryItem");
await click(".o-mail-DiscussSidebar i[title='Start a conversation']");
await insertText(".o-discuss-ChannelSelector", "Panda");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
assert.containsOnce($, ".o-discuss-ChannelSelector-suggestion");
await click(".o-mail-DiscussSidebar");
assert.containsNone($, ".o-discuss-ChannelSelector-suggestion");
@@ -187,12 +204,14 @@ QUnit.test("Chat is added to discuss on other tab that the one that joined", asy
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Jerry Golay" });
pyEnv["res.users"].create({ partner_id: partnerId });
const tab1 = await start({ asTab: true });
const tab1 = await start({ asTab: true, hasTimeControl: true });
const tab2 = await start({ asTab: true });
await tab1.openDiscuss();
await tab2.openDiscuss();
await tab1.click(".o-mail-DiscussCategory-chat .o-mail-DiscussCategory-add");
await tab1.insertText(".o-discuss-ChannelSelector input", "Jer");
await tab1.advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await tab1.click(".o-discuss-ChannelSelector-suggestion");
await afterNextRender(() => triggerHotkey("Enter"));
assert.containsOnce(tab1.target, ".o-mail-DiscussCategoryItem:contains(Jerry Golay)");
@@ -9,7 +9,8 @@ import {
} from "@mail/../tests/helpers/test_utils";
import { patchUiSize } from "@mail/../tests/helpers/patch_ui_size";
import { triggerHotkey } from "@web/../tests/helpers/utils";
import { nextTick, triggerHotkey } from "@web/../tests/helpers/utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
QUnit.module("messaging menu");
@@ -18,11 +19,13 @@ QUnit.test('"Start a conversation" item selection opens chat', async (assert) =>
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Gandalf" });
pyEnv["res.users"].create({ partner_id: partnerId });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
await click("button:contains(Chat)");
await click("button:contains(Start a conversation)");
await insertText("input[placeholder='Start a conversation']", "Gandalf");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
await afterNextRender(() => triggerHotkey("Enter"));
assert.containsOnce($, ".o-mail-ChatWindow-name[title='Gandalf']");
@@ -32,22 +35,26 @@ QUnit.test('"New channel" item selection opens channel (existing)', async (asser
patchUiSize({ height: 360, width: 640 });
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({ name: "Gryffindors" });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
await click("button:contains(Channel)");
await click("button:contains(New Channel)");
await insertText("input[placeholder='Add or join a channel']", "Gryff");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
assert.containsOnce($, ".o-mail-ChatWindow-name[title='Gryffindors']");
});
QUnit.test('"New channel" item selection opens channel (new)', async (assert) => {
patchUiSize({ height: 360, width: 640 });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
await click("button:contains(Channel)");
await click("button:contains(New Channel)");
await insertText("input[placeholder='Add or join a channel']", "slytherins");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await click(".o-discuss-ChannelSelector-suggestion");
assert.containsOnce($, ".o-mail-ChatWindow-name[title='slytherins']");
});
@@ -8,6 +8,7 @@ import {
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { makeDeferred } from "@web/../tests/helpers/utils";
@@ -22,7 +23,8 @@ QUnit.test("sidebar find shows channels matching search term", async (assert) =>
name: "test",
});
const def = makeDeferred();
const { openDiscuss } = await start({
const { advanceTime, openDiscuss } = await start({
hasTimeControl: true,
async mockRPC(route, args) {
if (args.method === "search_read") {
def.resolve();
@@ -32,6 +34,7 @@ QUnit.test("sidebar find shows channels matching search term", async (assert) =>
await openDiscuss();
await click(".o-mail-DiscussCategory-add");
await insertText(".o-discuss-ChannelSelector input", "test");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await def;
await nextAnimationFrame(); // ensures search_read rpc is rendered.
// When searching for a single existing channel, the results list will have at least 2 lines:
@@ -52,7 +55,8 @@ QUnit.test(
name: "test",
});
const def = makeDeferred();
const { openDiscuss } = await start({
const { advanceTime, openDiscuss } = await start({
hasTimeControl: true,
async mockRPC(route, args) {
if (args.method === "search_read") {
def.resolve();
@@ -62,6 +66,7 @@ QUnit.test(
await openDiscuss();
await click(".o-mail-DiscussCategory-add");
await insertText(".o-discuss-ChannelSelector input", "test");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await def;
await nextAnimationFrame(); // ensures search_read rpc is rendered.
// When searching for a single existing channel, the results list will have at least 2 lines:
@@ -7,8 +7,14 @@ import {
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
import { patchWithCleanup, triggerHotkey, mockTimeout } from "@web/../tests/helpers/utils";
import {
patchWithCleanup,
triggerHotkey,
mockTimeout,
nextTick,
} from "@web/../tests/helpers/utils";
QUnit.module("discuss inbox");
@@ -57,7 +63,7 @@ QUnit.test("reply: discard on pressing escape", async (assert) => {
notification_type: "inbox",
res_partner_id: pyEnv.currentPartnerId,
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss();
assert.containsOnce($, ".o-mail-Message");
@@ -73,6 +79,9 @@ QUnit.test("reply: discard on pressing escape", async (assert) => {
// Escape on suggestion prompt does not stop replying
await insertText(".o-mail-Composer-input", "@");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open");
await afterNextRender(() => triggerHotkey("Escape"));
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
@@ -16,12 +16,14 @@ import { url } from "@web/core/utils/urls";
import {
editInput,
makeDeferred,
nextTick,
patchWithCleanup,
triggerEvent,
triggerHotkey,
} from "@web/../tests/helpers/utils";
const { DateTime } = luxon;
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
QUnit.module("message");
@@ -319,11 +321,14 @@ QUnit.test("can add new mentions when editing message", async (assert) => {
res_id: channelId,
message_type: "comment",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await click(".o-mail-Message [title='Expand']");
await click(".o-mail-Message [title='Edit']");
await insertText(".o-mail-Composer-input", " @");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:contains(TestPartner)");
await click(".o-mail-Message a:contains('save')");
assert.strictEqual($(".o-mail-Message-body")[0].innerText, "Hello @TestPartner");
@@ -1236,12 +1241,15 @@ QUnit.test("Chat with partner should be opened after clicking on their mention",
email: "testpartner@odoo.com",
});
pyEnv["res.users"].create({ partner_id: partnerId });
const { openFormView } = await start();
const { advanceTime, openFormView } = await start({ hasTimeControl: true });
await openFormView("res.partner", partnerId);
await click("button:contains(Send message)");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "T");
await insertText(".o-mail-Composer-input", "e");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:contains(Test Partner)");
await click(".o-mail-Composer-send");
await click(".o_mail_redirect");
@@ -1284,10 +1292,13 @@ QUnit.test("Channel should be opened after clicking on its mention", async (asse
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({});
pyEnv["discuss.channel"].create({ name: "my-channel" });
const { openFormView } = await start();
const { advanceTime, openFormView } = await start({ hasTimeControl: true });
await openFormView("res.partner", partnerId);
await click("button:contains(Send message)");
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:contains(my-channel)");
await click(".o-mail-Composer-send");
await click(".o_channel_redirect");
@@ -10,6 +10,7 @@ import {
patchWithCleanup,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
QUnit.module("suggestion", {
async beforeEach() {
@@ -41,11 +42,14 @@ QUnit.test('display partner mention suggestions on typing "@"', async (assert) =
Command.create({ partner_id: partnerId_2 }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestion");
await insertText(".o-mail-Composer-input", "@");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsN($, ".o-mail-Composer-suggestion", 3);
});
@@ -70,7 +74,7 @@ QUnit.test(
Command.create({ partner_id: partnerId_2 }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestion");
await insertText(".o-mail-Composer-input", "first message");
@@ -78,17 +82,23 @@ QUnit.test(
await nextTick();
await insertText(".o-mail-Composer-input", "@");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsN($, ".o-mail-Composer-suggestion", 3);
}
);
QUnit.test('display partner mention suggestions on typing "@" in chatter', async (assert) => {
const pyEnv = await startServer();
const { openFormView } = await start();
const { advanceTime, openFormView } = await start({ hasTimeControl: true });
await openFormView("res.partner", pyEnv.currentPartnerId);
await click("button:contains(Send message)");
assert.containsNone($, ".o-mail-Composer-suggestion");
await insertText(".o-mail-Composer-input", "@");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion:contains(Mitchell Admin)");
});
@@ -105,9 +115,12 @@ QUnit.test("show other channel member in @ mention", async (assert) => {
Command.create({ partner_id: partnerId }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion:contains(TestPartner)");
});
@@ -124,9 +137,12 @@ QUnit.test("select @ mention insert mention text in composer", async (assert) =>
Command.create({ partner_id: partnerId }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:contains(TestPartner)");
assert.strictEqual($(".o-mail-Composer-input").val().trim(), "@TestPartner");
});
@@ -137,10 +153,13 @@ QUnit.test('display channel mention suggestions on typing "#"', async (assert) =
name: "General",
channel_type: "channel",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open");
});
@@ -150,11 +169,14 @@ QUnit.test("mention a channel", async (assert) => {
name: "General",
channel_type: "channel",
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
assert.containsNone($, ".o-mail-Composer-suggestionList .o-open");
assert.strictEqual($(".o-mail-Composer-input").val(), "");
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
assert.containsOnce($, ".o-mail-Composer-suggestion");
await click(".o-mail-Composer-suggestion");
assert.strictEqual(
@@ -21,6 +21,7 @@ import {
patchWithCleanup,
triggerEvents,
} from "@web/../tests/helpers/utils";
import { DEBOUNCE_FETCH_SUGGESTION_TIME } from "@mail/core/common/suggestion_service";
QUnit.module("thread");
@@ -208,9 +209,12 @@ QUnit.test("thread is still scrolling after scrolling up then to bottom", async
QUnit.test("mention a channel with space in the name", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General good boy" });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce($(".o-mail-Message-body"), ".o_channel_redirect");
@@ -220,9 +224,12 @@ QUnit.test("mention a channel with space in the name", async (assert) => {
QUnit.test('mention a channel with "&" in the name', async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General & good" });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce($(".o-mail-Message-body"), ".o_channel_redirect");
@@ -543,10 +550,13 @@ QUnit.test("Mention a partner with special character (e.g. apostrophe ')", async
Command.create({ partner_id: partnerId }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Pyn");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce(
@@ -575,13 +585,19 @@ QUnit.test("mention 2 different partners that have the same name", async (assert
Command.create({ partner_id: partnerId_2 }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Te");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:eq(0)");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Te");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:eq(1)");
await click(".o-mail-Composer-send");
assert.containsOnce($, ".o-mail-Message-body");
@@ -598,10 +614,13 @@ QUnit.test("mention 2 different partners that have the same name", async (assert
QUnit.test("mention a channel on a second line when the first line contains #", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General good" });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#blabla\n");
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce($(".o-mail-Message-body"), ".o_channel_redirect");
@@ -613,9 +632,12 @@ QUnit.test(
async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General good" });
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion");
const text = $(".o-mail-Composer-input").val();
$(".o-mail-Composer-input").val(text.slice(0, -1));
@@ -639,13 +661,19 @@ QUnit.test("mention 2 different channels that have the same name", async (assert
name: "my channel",
},
]);
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId_1);
await insertText(".o-mail-Composer-input", "#");
await insertText(".o-mail-Composer-input", "m");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:eq(0)");
await insertText(".o-mail-Composer-input", "#");
await insertText(".o-mail-Composer-input", "m");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion:eq(1)");
await click(".o-mail-Composer-send");
assert.containsOnce($, ".o-mail-Message-body");
@@ -674,11 +702,14 @@ QUnit.test(
Command.create({ partner_id: partnerId }),
],
});
const { openDiscuss } = await start();
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "email@odoo.com\n");
await insertText(".o-mail-Composer-input", "@");
await insertText(".o-mail-Composer-input", "Te");
await advanceTime(DEBOUNCE_FETCH_SUGGESTION_TIME);
await nextTick();
await nextTick();
await click(".o-mail-Composer-suggestion");
await click(".o-mail-Composer-send");
assert.containsOnce(