[REF] mail: tests: add scrollTop to contains

This is much more robust and faster than relying on arbitrary number of
animation frames.

closes odoo/odoo#133333

Related: odoo/enterprise#46407
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2023-08-29 11:10:52 +00:00
parent ea9615cce6
commit fa8f68ce05
12 changed files with 283 additions and 321 deletions
+10 -3
View File
@@ -127,19 +127,26 @@ export function useAutoScroll(refName, shouldScrollPredicate = () => true) {
const ref = useRef(refName);
let el = null;
let isScrolled = true;
let lastSetValue;
const observer = new ResizeObserver(applyScroll);
function onScroll() {
isScrolled = Math.abs(el.scrollTop + el.clientHeight - el.scrollHeight) < 1;
}
function applyScroll() {
if (isScrolled && shouldScrollPredicate()) {
async function applyScroll() {
if (isScrolled && shouldScrollPredicate() && lastSetValue !== ref.el.scrollHeight) {
/**
* Avoid setting the same value 2 times in a row. This is not supposed to have an
* effect, unless the value was changed from outside in the meantime, in which case
* resetting the value would incorrectly override the other change.
*/
lastSetValue = ref.el.scrollHeight;
ref.el.scrollTop = ref.el.scrollHeight;
}
}
onMounted(() => {
el = ref.el;
el.scrollTop = el.scrollHeight;
applyScroll();
observer.observe(el);
el.addEventListener("scroll", onScroll);
});
@@ -8,13 +8,11 @@ import {
import { Command } from "@mail/../tests/helpers/command";
import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size";
import {
afterNextRender,
click,
contains,
createFile,
insertText,
isScrolledToBottom,
nextAnimationFrame,
scroll,
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
@@ -774,7 +772,7 @@ QUnit.test("chat window should not open when receiving a new DM from odoobot", a
QUnit.test(
"chat window should scroll to the newly posted message just after posting it",
async (assert) => {
async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
@@ -797,9 +795,11 @@ QUnit.test(
});
}
await start();
await contains(".o-mail-Message", 10);
await insertText(".o-mail-Composer-input", "WOLOLO");
triggerHotkey("Enter");
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
await contains(".o-mail-Message", 11);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
}
);
@@ -967,7 +967,7 @@ QUnit.test(
}
);
QUnit.test("chat window: scroll conservation on toggle discuss", async (assert) => {
QUnit.test("chat window: scroll conservation on toggle discuss", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({});
for (let i = 0; i < 100; i++) {
@@ -979,26 +979,24 @@ QUnit.test("chat window: scroll conservation on toggle discuss", async (assert)
}
const { openDiscuss, openView } = await start();
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
/**
* The afterNextRender is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await afterNextRender(async () => await click(".o-mail-NotificationItem"));
(await contains(".o-mail-ChatWindow .o-mail-Thread"))[0].scrollTop = 142;
await click(".o-mail-NotificationItem");
await contains(".o-mail-Message", 31);
await contains(".o-mail-ChatWindow .o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-ChatWindow .o-mail-Thread", 142);
openDiscuss(null, { waitUntilMessagesLoaded: false });
await contains(".o-mail-ChatWindow", 0);
openView({
res_id: channelId,
res_model: "discuss.channel",
views: [[false, "list"]],
});
assert.strictEqual((await contains(".o-mail-ChatWindow .o-mail-Thread"))[0].scrollTop, 142);
await contains(".o-mail-Message", 31);
await contains(".o-mail-ChatWindow .o-mail-Thread", 1, { scroll: 142 });
});
QUnit.test(
"chat window with a thread: keep scroll position in message list on folded",
async (assert) => {
async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({});
for (let i = 0; i < 100; i++) {
@@ -1010,25 +1008,24 @@ QUnit.test(
}
await start();
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
/**
* The afterNextRender is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await afterNextRender(async () => await click(".o-mail-NotificationItem"));
(await contains(".o-mail-ChatWindow .o-mail-Thread"))[0].scrollTop = 142;
await click(".o-mail-NotificationItem");
await contains(".o-mail-Message", 31);
await contains(".o-mail-ChatWindow .o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-ChatWindow .o-mail-Thread", 142);
// fold chat window
await click(".o-mail-ChatWindow-command[title='Fold']");
await contains(".o-mail-Message", 0);
await contains(".o-mail-ChatWindow .o-mail-Thread", 0);
// unfold chat window
await click(".o-mail-ChatWindow-command[title='Open']");
assert.strictEqual((await contains(".o-mail-ChatWindow .o-mail-Thread"))[0].scrollTop, 142);
await contains(".o-mail-Message", 31);
await contains(".o-mail-ChatWindow .o-mail-Thread", 1, { scroll: 142 });
}
);
QUnit.test(
"chat window with a thread: keep scroll position in message list on toggle discuss when folded",
async (assert) => {
async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({});
for (let i = 0; i < 100; i++) {
@@ -1040,18 +1037,14 @@ QUnit.test(
}
const { openDiscuss, openView } = await start();
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
/**
* The afterNextRender is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await afterNextRender(async () => await click(".o-mail-NotificationItem"));
(await contains(".o-mail-ChatWindow .o-mail-Thread"))[0].scrollTop = 142;
await click(".o-mail-NotificationItem");
await contains(".o-mail-Message", 31);
await contains(".o-mail-ChatWindow .o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-ChatWindow .o-mail-Thread", 142);
// fold chat window
await click(".o-mail-ChatWindow-command[title='Fold']");
openDiscuss(null, { waitUntilMessagesLoaded: false });
await contains(".o-mail-ChatWindow", 0);
openView({
res_id: channelId,
res_model: "discuss.channel",
@@ -1059,7 +1052,8 @@ QUnit.test(
});
// unfold chat window
await click(".o-mail-ChatWindow-command[title='Open']");
assert.strictEqual((await contains(".o-mail-ChatWindow .o-mail-Thread"))[0].scrollTop, 142);
await contains(".o-mail-ChatWindow .o-mail-Message", 31);
await contains(".o-mail-ChatWindow .o-mail-Thread", 1, { scroll: 142 });
}
);
@@ -1102,8 +1096,7 @@ QUnit.test("Chat window in mobile are not foldable", async () => {
await click("button i[aria-label='Messages']");
await click(".o-mail-NotificationItem");
await contains(".o-mail-ChatWindow-header.cursor-pointer", 0);
click(".o-mail-ChatWindow-header").catch(() => {});
await nextAnimationFrame();
await click(".o-mail-ChatWindow-header");
await contains(".o-mail-ChatWindow-content"); // content => non-folded
});
@@ -12,6 +12,7 @@ import {
dropFiles,
insertText,
pasteFiles,
scroll,
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
@@ -196,33 +197,33 @@ QUnit.test(
}
);
QUnit.test("keep emoji picker scroll value when re-opening it", async (assert) => {
QUnit.test("keep emoji picker scroll value when re-opening it", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "roblox-carsurfing" });
const { openDiscuss } = await start();
openDiscuss(channelId);
await click("button[aria-label='Emojis']");
(await contains(".o-EmojiPicker-content"))[0].scrollTop = 150;
await scroll(".o-EmojiPicker-content", 150);
await click("button[aria-label='Emojis']");
await click("button[aria-label='Emojis']");
assert.strictEqual((await contains(".o-EmojiPicker-content"))[0].scrollTop, 150);
await contains(".o-EmojiPicker-content", 1, { scroll: 150 });
});
QUnit.test("reset emoji picker scroll value after an emoji is picked", async (assert) => {
QUnit.test("reset emoji picker scroll value after an emoji is picked", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "roblox-fingerskating" });
const { openDiscuss } = await start();
openDiscuss(channelId);
await click("button[aria-label='Emojis']");
(await contains(".o-EmojiPicker-content"))[0].scrollTop = 150;
await scroll(".o-EmojiPicker-content", 150);
await click(".o-Emoji:contains(😎)");
await click("button[aria-label='Emojis']");
assert.strictEqual((await contains(".o-EmojiPicker-content"))[0].scrollTop, 0);
await contains(".o-EmojiPicker-content", 1, { scroll: 0 });
});
QUnit.test(
"keep emoji picker scroll value independent if two or more different emoji pickers are used",
async (assert) => {
async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "roblox-jaywalking" });
const { openDiscuss } = await start();
@@ -235,22 +236,18 @@ QUnit.test(
res_id: channelId,
});
openDiscuss(channelId);
await triggerEvent(getFixture(), null, "mousedown");
await click("button[aria-label='Emojis']");
(await contains(".o-EmojiPicker-content"))[0].scrollTop = 150;
await scroll(".o-EmojiPicker-content", 150);
await triggerEvent(getFixture(), null, "mousedown");
await click("[title='Add a Reaction']");
(await contains(".o-EmojiPicker-content"))[0].scrollTop = 200;
await scroll(".o-EmojiPicker-content", 200);
await triggerEvent(getFixture(), null, "mousedown");
await click("button[aria-label='Emojis']");
assert.strictEqual((await contains(".o-EmojiPicker-content"))[0].scrollTop, 150);
await contains(".o-EmojiPicker-content", 1, { scroll: 150 });
await triggerEvent(getFixture(), null, "mousedown");
await click("[title='Add a Reaction']");
assert.strictEqual((await contains(".o-EmojiPicker-content"))[0].scrollTop, 200);
await contains(".o-EmojiPicker-content", 1, { scroll: 200 });
}
);
@@ -11,8 +11,7 @@ import {
contains,
createFile,
insertText,
isScrolledToBottom,
nextAnimationFrame,
scroll,
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
@@ -261,7 +260,7 @@ QUnit.test("No load more when fetch below fetch limit of 30", async (assert) =>
});
openDiscuss(channelId);
await contains(".o-mail-Message", 29);
await contains("button:contains(Load more)", 0);
await contains("button:contains(Load More)", 0);
});
QUnit.test("show date separator above mesages of similar date", async (assert) => {
@@ -796,17 +795,13 @@ QUnit.test('all messages in "Inbox" in "History" after marked all as read', asyn
}
const { openDiscuss } = await start();
openDiscuss();
await contains(".o-mail-Message", 30);
await click("button:contains(Mark all read)");
await contains(".o-mail-Message", 0);
/**
* The afterNextRender is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await afterNextRender(async () => await click("button:contains(History)"));
await click("button:contains(History)");
await contains(".o-mail-Message", 30);
$(".o-mail-Thread")[0].scrollTop = 0;
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", 0);
await contains(".o-mail-Message", 40);
});
@@ -1439,27 +1434,23 @@ QUnit.test("new messages separator [REQUIRE FOCUS]", async () => {
openDiscuss(channelId);
await contains(".o-mail-Message", 25);
await contains("hr + span:contains(New messages)", 0);
$(".o-mail-Discuss-content .o-mail-Thread")[0].scrollTop = 0;
await contains(".o-mail-Discuss-content .o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Discuss-content .o-mail-Thread", 0);
// composer is focused by default, we remove that focus
$(".o-mail-Composer-input")[0].blur();
// simulate receiving a message
await afterNextRender(async () =>
pyEnv.withUser(userId, () =>
env.services.rpc("/mail/message/post", {
post_data: { body: "hu", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
)
pyEnv.withUser(userId, () =>
env.services.rpc("/mail/message/post", {
post_data: { body: "hu", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-Message", 26);
await contains("hr + span:contains(New messages)");
const messageList = $(".o-mail-Discuss-content .o-mail-Thread")[0];
messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight;
await scroll(".o-mail-Discuss-content .o-mail-Thread", "bottom");
await contains("hr + span:contains(New messages)");
await afterNextRender(() => $(".o-mail-Composer-input")[0].focus());
$(".o-mail-Composer-input")[0].focus();
await contains("hr + span:contains(New messages)", 0);
});
@@ -1654,7 +1645,7 @@ QUnit.test(
}
);
QUnit.test("restore thread scroll position", async (assert) => {
QUnit.test("restore thread scroll position", async () => {
const pyEnv = await startServer();
const [channelId_1, channelId_2] = pyEnv["discuss.channel"].create([
{ name: "Channel1" },
@@ -1677,30 +1668,17 @@ QUnit.test("restore thread scroll position", async (assert) => {
const { openDiscuss } = await start();
openDiscuss(channelId_1);
await contains(".o-mail-Message", 25);
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
$(".o-mail-Thread")[0].scrollTop = 0;
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", 0);
await click("button:contains(Channel2)");
await contains(".o-mail-Message", 24);
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await click("button:contains(Channel1)");
/**
* The nextAnimationFrame is necessary because otherwise the scroll position
* would not be restored yet.
*/
await nextAnimationFrame();
assert.strictEqual($(".o-mail-Thread")[0].scrollTop, 0);
await contains(".o-mail-Message", 25);
await contains(".o-mail-Thread", 1, { scroll: 0 });
await click("button:contains(Channel2)");
/**
* The nextAnimationFrame is necessary because otherwise the scroll position
* would not be restored yet.
*/
await nextAnimationFrame();
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
await contains(".o-mail-Message", 24);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
});
QUnit.test("Message shows up even if channel data is incomplete", async () => {
@@ -1,15 +1,9 @@
/* @odoo-module */
import { PRESENT_THRESHOLD } from "@mail/core/common/thread";
import {
click,
contains,
nextAnimationFrame,
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
import { click, contains, scroll, start, startServer } from "@mail/../tests/helpers/test_utils";
import { nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
QUnit.module("jump to present");
@@ -22,11 +16,6 @@ QUnit.test("Basic jump to present when scrolling to outdated messages", async (a
});
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Hello world!",
model: "discuss.channel",
res_id: channelId,
});
for (let i = 0; i < 20; i++) {
pyEnv["mail.message"].create({
body: "Non Empty Body ".repeat(100),
@@ -37,24 +26,17 @@ QUnit.test("Basic jump to present when scrolling to outdated messages", async (a
}
const { openDiscuss } = await start();
openDiscuss(channelId);
await contains(".o-mail-Message", 20);
assert.ok(
(await contains(".o-mail-Thread"))[0].scrollHeight > PRESENT_THRESHOLD,
"should have enough scroll height to trigger jump to present"
);
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
$(".o-mail-Thread").scrollTop(0);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", 0);
await contains(".o-mail-Thread:contains(You're viewing older messagesJump to Present)");
await click(".o-mail-Thread-jumpPresent");
await nextTick();
await contains(".o-mail-Thread:contains(You're viewing older messagesJump to Present)", 0);
assert.ok(
$(".o-mail-Thread")[0].scrollHeight - $(".o-mail-Thread")[0].scrollTop <=
$(".o-mail-Thread")[0].clientHeight
);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
});
QUnit.test("Jump to old reply should prompt jump to presence", async (assert) => {
@@ -94,8 +76,9 @@ QUnit.test("Jump to old reply should prompt jump to presence", async (assert) =>
});
const { openDiscuss } = await start();
openDiscuss(channelId);
await contains(".o-mail-Message", 30);
await click(".o-mail-MessageInReply .cursor-pointer");
await nextAnimationFrame(); // wait for scroll into view
await contains(".o-mail-Message", 46);
assert.isVisible((await contains(".o-mail-Message:contains(Hello world!):eq(0)"))[0]);
assert.strictEqual(
$(".o-mail-Message-body:contains(Hello world!):eq(0)").text(),
@@ -105,8 +88,6 @@ QUnit.test("Jump to old reply should prompt jump to presence", async (assert) =>
await contains(".o-mail-Thread:contains(You're viewing older messagesJump to Present)");
await click(".o-mail-Thread-jumpPresent");
await contains(".o-mail-Thread:contains(You're viewing older messagesJump to Present)", 0);
assert.ok(
$(".o-mail-Thread")[0].scrollHeight - $(".o-mail-Thread")[0].scrollTop <=
$(".o-mail-Thread")[0].clientHeight
);
await contains(".o-mail-Message", 30);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
});
+59 -4
View File
@@ -599,8 +599,6 @@ export {
afterNextRender,
dragenterFiles,
dropFiles,
isScrolledToBottom,
isScrolledTo,
nextAnimationFrame,
nextTick,
pasteFiles,
@@ -620,6 +618,19 @@ export async function click(selector, { target = document.body } = {}) {
await contains(selector, 1, { click: true, target });
}
/**
* Waits until exactly one element matching the given selector is present in
* `options.target` and then sets its `scrollTop` to the given value.
*
* @param {string} selector
* @param {number|"bottom"} scrollTop
* @param {Object} [options={}]
* @param {HTMLElement} [options.target=document.body]
*/
export async function scroll(selector, scrollTop, { target = document.body } = {}) {
await contains(selector, 1, { setScroll: scrollTop, target });
}
let hasUsedContainsPositively = false;
QUnit.testStart(() => (hasUsedContainsPositively = false));
/**
@@ -630,12 +641,21 @@ QUnit.testStart(() => (hasUsedContainsPositively = false));
* @param {number} [count=1]
* @param {Object} [options={}]
* @param {boolean} [options.click] if provided, clicks on the found element
* @param {number|"bottom"} [options.scroll] if provided, the scrollTop of the found element(s)
* must match.
* Note: when using one of the scrollTop options, it is advised to ensure the height is not going
* to change soon, by checking with a preceding contains that all the expected elements are in DOM.
* @param {number|"bottom"} [options.setScroll] if provided, set the scrollTop on the found element
* @param {HTMLElement} [options.target=document.body]
* @param {string} [options.value] if provided, the input value of the found element(s) must match.
* Note: value changes are not observed directly, another mutation must happen to catch them.
* @returns {Promise<JQuery<HTMLElement>>}
*/
export function contains(selector, count = 1, { click, target = document.body, value } = {}) {
export function contains(
selector,
count = 1,
{ click, scroll, setScroll, target = document.body, value } = {}
) {
if (count) {
hasUsedContainsPositively = true;
} else if (!hasUsedContainsPositively) {
@@ -644,10 +664,14 @@ export function contains(selector, count = 1, { click, target = document.body, v
);
}
return new Promise((resolve, reject) => {
const scrollListeners = new Set();
let selectorMessage = `${count} of "${selector}"`;
if (value !== undefined) {
selectorMessage = `${selectorMessage} with value "${value}"`;
}
if (scroll !== undefined) {
selectorMessage = `${selectorMessage} with scroll "${scroll}"`;
}
const $res = select();
if ($res.length === count) {
execute($res, "immediately");
@@ -682,9 +706,33 @@ export function contains(selector, count = 1, { click, target = document.body, v
reject(new Error(message));
}
});
function onScroll(ev) {
const $res = select();
if ($res.length === count) {
clean();
execute($res, "after scroll");
}
}
function select() {
const $res = $(target).find(selector);
return value === undefined ? $res : $res.filter((i, el) => el.value === value);
const $filteredRes = $res.filter(
(i, el) =>
(value === undefined || el.value === value) &&
(scroll === undefined ||
(scroll === "bottom" ? isScrolledToBottom(el) : isScrolledTo(el, scroll)))
);
if (
scroll !== undefined &&
!scrollListeners.size &&
$res.length === count &&
$filteredRes.length !== count
) {
for (const el of $res) {
scrollListeners.add(el);
el.addEventListener("scroll", onScroll);
}
}
return $filteredRes;
}
function execute($res, whenMessage) {
let message = `Found ${selectorMessage} (${whenMessage})`;
@@ -692,12 +740,19 @@ export function contains(selector, count = 1, { click, target = document.body, v
message = `${message} and clicked it`;
$res[0].click();
}
if (setScroll !== undefined) {
message = `${message} and set scroll to "${setScroll}"`;
$res[0].scrollTop = setScroll === "bottom" ? $res[0].scrollHeight : setScroll;
}
QUnit.assert.ok(true, message);
resolve($res);
}
function clean() {
observer.disconnect();
clearTimeout(timer);
for (const el of scrollListeners) {
el.removeEventListener("scroll", onScroll);
}
done = true;
}
});
@@ -287,10 +287,10 @@ QUnit.test("Scroll bar to the top when edit starts", async (assert) => {
await click(".o-mail-Message [title='Edit']");
const $textarea = await contains(".o-mail-Message .o-mail-Composer-input");
assert.ok($textarea[0].scrollHeight > $textarea[0].clientHeight);
assert.strictEqual($textarea[0].scrollTop, 0);
await contains(".o-mail-Message .o-mail-Composer-input", 1, { scroll: 0 });
});
QUnit.test("mentions are kept when editing message", async (assert) => {
QUnit.test("mentions are kept when editing message", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "general",
+105 -132
View File
@@ -7,9 +7,7 @@ import {
contains,
dragenterFiles,
insertText,
isScrolledTo,
isScrolledToBottom,
nextAnimationFrame,
scroll,
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
@@ -54,12 +52,9 @@ QUnit.test("load more messages from channel (auto-load on scroll)", async () =>
const { openDiscuss } = await start();
openDiscuss(channelId);
await contains("button:contains(Load More) ~ .o-mail-Message", 30);
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
$(".o-mail-Thread")[0].scrollTop = 0;
await contains(".o-mail-Message", 30);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", 0);
await contains(".o-mail-Message", 60);
});
@@ -100,7 +95,7 @@ QUnit.test("do not show message subject when subject is the same as the thread n
await contains(".o-mail-Message:not(:contains(Salutations, voyageur))");
});
QUnit.test("auto-scroll to bottom of thread on load", async (assert) => {
QUnit.test("auto-scroll to bottom of thread on load", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
for (let i = 1; i <= 25; i++) {
@@ -113,7 +108,7 @@ QUnit.test("auto-scroll to bottom of thread on load", async (assert) => {
const { openDiscuss } = await start();
openDiscuss(channelId);
await contains(".o-mail-Message", 25);
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
});
QUnit.test("display day separator before first message of the day", async () => {
@@ -150,53 +145,42 @@ QUnit.test("do not display day separator if all messages of the day are empty",
await contains(".o-mail-Thread-date", 0);
});
QUnit.test(
"scroll position is kept when navigating from one channel to another",
async (assert) => {
const pyEnv = await startServer();
const channelId_1 = pyEnv["discuss.channel"].create({ name: "channel-1" });
const channelId_2 = pyEnv["discuss.channel"].create({ name: "channel-2" });
// Fill both channels with random messages in order for the scrollbar to
// appear.
pyEnv["mail.message"].create(
Array(40)
.fill(0)
.map((_, index) => ({
body: "Non Empty Body ".repeat(25),
message_type: "comment",
model: "discuss.channel",
res_id: index & 1 ? channelId_1 : channelId_2,
}))
);
const { openDiscuss } = await start();
openDiscuss(channelId_1);
await contains(".o-mail-Thread");
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
const scrolltop_1 = $(".o-mail-Thread")[0].scrollHeight / 2;
$(".o-mail-Thread")[0].scrollTo({ top: scrolltop_1 });
await click(".o-mail-DiscussSidebarChannel:contains(channel-2)");
await contains(".o-mail-DiscussSidebarChannel:contains(channel-2).o-active");
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
const scrolltop_2 = $(".o-mail-Thread")[0].scrollHeight / 3;
$(".o-mail-Thread")[0].scrollTo({ top: scrolltop_2 });
await click(".o-mail-DiscussSidebarChannel:contains(channel-1)");
await contains(".o-mail-DiscussSidebarChannel:contains(channel-1).o-active");
assert.ok(isScrolledTo($(".o-mail-Thread")[0], scrolltop_1));
await click(".o-mail-DiscussSidebarChannel:contains(channel-2)");
await contains(".o-mail-DiscussSidebarChannel:contains(channel-2).o-active");
assert.ok(isScrolledTo($(".o-mail-Thread")[0], scrolltop_2));
}
);
QUnit.test("scroll position is kept when navigating from one channel to another", async () => {
const pyEnv = await startServer();
const channelId_1 = pyEnv["discuss.channel"].create({ name: "channel-1" });
const channelId_2 = pyEnv["discuss.channel"].create({ name: "channel-2" });
// Fill both channels with random messages in order for the scrollbar to
// appear.
pyEnv["mail.message"].create(
Array(50)
.fill(0)
.map((_, index) => ({
body: "Non Empty Body ".repeat(25),
message_type: "comment",
model: "discuss.channel",
res_id: index < 20 ? channelId_1 : channelId_2,
}))
);
const { openDiscuss } = await start();
openDiscuss(channelId_1);
await contains(".o-mail-Message", 20);
const scrollValue1 = $(".o-mail-Thread")[0].scrollHeight / 2;
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", scrollValue1);
await click(".o-mail-DiscussSidebarChannel:contains(channel-2)");
await contains(".o-mail-Message", 30);
const scrollValue2 = $(".o-mail-Thread")[0].scrollHeight / 3;
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", scrollValue2);
await click(".o-mail-DiscussSidebarChannel:contains(channel-1)");
await contains(".o-mail-Message", 20);
await contains(".o-mail-Thread", 1, { scroll: scrollValue1 });
await click(".o-mail-DiscussSidebarChannel:contains(channel-2)");
await contains(".o-mail-Message", 30);
await contains(".o-mail-Thread", 1, { scroll: scrollValue2 });
});
QUnit.test("thread is still scrolling after scrolling up then to bottom", async (assert) => {
QUnit.test("thread is still scrolling after scrolling up then to bottom", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "channel-1" });
pyEnv["mail.message"].create(
@@ -211,19 +195,14 @@ QUnit.test("thread is still scrolling after scrolling up then to bottom", async
);
const { openDiscuss } = await start();
openDiscuss(channelId);
await contains(".o-mail-Thread");
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
$(".o-mail-Thread")[0].scrollTo({ top: $(".o-mail-Thread")[0].scrollHeight / 2 });
$(".o-mail-Thread")[0].scrollTo({ top: $(".o-mail-Thread")[0].scrollHeight });
await contains(".o-mail-Message", 20);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", $(".o-mail-Thread")[0].scrollHeight / 2);
await scroll(".o-mail-Thread", "bottom");
await insertText(".o-mail-Composer-input", "123");
await click(".o-mail-Composer-send:not(:disabled)");
await contains(".o-mail-Message", 21);
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
});
QUnit.test("mention a channel with space in the name", async () => {
@@ -279,17 +258,19 @@ QUnit.test(
}
},
});
await contains(".o_menu_systray i[aria-label='Messages']");
pyEnv.withUser(userId, () =>
env.services.rpc("/mail/message/post", {
post_data: { body: "new message", message_type: "comment" },
post_data: { body: "Hello!", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-Message");
assert.verifySteps(["rpc:channel_fetch"]);
await contains("hr + span:contains(New messages)");
$(".o-mail-Composer-input")[0].focus();
await contains("span:contains(New messages)", 0);
await contains("hr + span:contains(New messages)", 0);
assert.verifySteps(["rpc:set_last_seen_message"]);
}
);
@@ -338,7 +319,7 @@ QUnit.test(
QUnit.test(
"should scroll to bottom on receiving new message if the list is initially scrolled to bottom (asc order)",
async (assert) => {
async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" });
const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId });
@@ -358,25 +339,24 @@ QUnit.test(
const { env } = await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem");
assert.ok(isScrolledToBottom((await contains(".o-mail-Thread"))[0]));
await contains(".o-mail-Message", 11);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
// simulate receiving a message
await afterNextRender(() =>
pyEnv.withUser(userId, () =>
env.services.rpc("/mail/message/post", {
post_data: { body: "hello", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
)
pyEnv.withUser(userId, () =>
env.services.rpc("/mail/message/post", {
post_data: { body: "hello", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
assert.ok(isScrolledToBottom($(".o-mail-Thread")[0]));
await contains(".o-mail-Message", 12);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
}
);
QUnit.test(
"should not scroll on receiving new message if the list is initially scrolled anywhere else than bottom (asc order)",
async (assert) => {
async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" });
const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId });
@@ -395,24 +375,20 @@ QUnit.test(
}
const { env } = await start();
await click(".o_menu_systray i[aria-label='Messages']");
/**
* The afterNextRender is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await afterNextRender(() => click(".o-mail-NotificationItem"));
assert.ok(isScrolledToBottom((await contains(".o-mail-Thread"))[0]));
$(".o-mail-Thread").scrollTop(0);
await click(".o-mail-NotificationItem");
await contains(".o-mail-Message", 11);
await contains(".o-mail-Thread", 1, { scroll: "bottom" });
await scroll(".o-mail-Thread", 0);
// simulate receiving a message
await afterNextRender(() =>
pyEnv.withUser(userId, () =>
env.services.rpc("/mail/message/post", {
post_data: { body: "hello", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
)
pyEnv.withUser(userId, () =>
env.services.rpc("/mail/message/post", {
post_data: { body: "hello", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
assert.strictEqual($(".o-mail-Thread")[0].scrollTop, 0);
await contains(".o-mail-Message", 12);
await contains(".o-mail-ChatWindow .o-mail-Thread", 1, { scroll: 0 });
}
);
@@ -437,7 +413,7 @@ QUnit.test("show empty placeholder when thread contains only empty messages", as
QUnit.test(
"message list with a full page of empty messages should load more messages until there are some non-empty",
async (assert) => {
async () => {
// Technical assumptions :
// - /discuss/channel/messages fetching exactly 30 messages,
// - empty messages not being displayed
@@ -462,40 +438,37 @@ QUnit.test(
}
);
QUnit.test(
"no new messages separator on posting message (some message history)",
async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ message_unread_counter: 0, partner_id: pyEnv.currentPartnerId }),
],
channel_type: "channel",
name: "General",
});
const messageId = pyEnv["mail.message"].create({
body: "first message",
model: "discuss.channel",
res_id: channelId,
});
const [memberId] = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", pyEnv.currentPartnerId],
]);
pyEnv["discuss.channel.member"].write([memberId], { seen_message_id: messageId });
const { openDiscuss } = await start();
openDiscuss(channelId);
await contains(".o-mail-Message");
await contains("hr + span:contains(New messages)", 0);
QUnit.test("no new messages separator on posting message (some message history)", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ message_unread_counter: 0, partner_id: pyEnv.currentPartnerId }),
],
channel_type: "channel",
name: "General",
});
const messageId = pyEnv["mail.message"].create({
body: "first message",
model: "discuss.channel",
res_id: channelId,
});
const [memberId] = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", pyEnv.currentPartnerId],
]);
pyEnv["discuss.channel.member"].write([memberId], { seen_message_id: messageId });
const { openDiscuss } = await start();
openDiscuss(channelId);
await contains(".o-mail-Message");
await contains("hr + span:contains(New messages)", 0);
await insertText(".o-mail-Composer-input", "hey!");
// need to remove focus from text area to avoid set_last_seen_message
(await contains(".o-mail-Composer-send:not(:disabled)"))[0].focus();
await click(".o-mail-Composer-send:not(:disabled)");
await contains(".o-mail-Message", 2);
await contains("hr + span:contains(New messages)", 0);
}
);
await insertText(".o-mail-Composer-input", "hey!");
// need to remove focus from text area to avoid set_last_seen_message
(await contains(".o-mail-Composer-send:not(:disabled)"))[0].focus();
await click(".o-mail-Composer-send:not(:disabled)");
await contains(".o-mail-Message", 2);
await contains("hr + span:contains(New messages)", 0);
});
QUnit.test("new messages separator on receiving new message [REQUIRE FOCUS]", async () => {
patchWithCleanup(transitionConfig, { disabled: true });
@@ -1,13 +1,7 @@
/* @odoo-module */
import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size";
import {
click,
contains,
nextAnimationFrame,
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
import { click, contains, scroll, start, startServer } from "@mail/../tests/helpers/test_utils";
QUnit.module("attachment box");
@@ -177,9 +171,11 @@ QUnit.test("scroll to attachment box when toggling on", async (assert) => {
res_model: "res.partner",
views: [[false, "form"]],
});
$(".o-mail-Chatter").scrollTop(10 * 1000); // to bottom
await contains(".o-mail-Message", 30);
await scroll(".o-mail-Chatter", "bottom");
await click("button[aria-label='Attach files']");
await nextAnimationFrame();
await contains(".o-mail-AttachmentBox");
await contains(".o-mail-Chatter", 1, { scroll: 0 });
assert.isVisible($(".o-mail-AttachmentBox"));
});
+15 -34
View File
@@ -8,8 +8,7 @@ import {
dragenterFiles,
dropFiles,
insertText,
isScrolledTo,
nextAnimationFrame,
scroll,
start,
startServer,
} from "@mail/../tests/helpers/test_utils";
@@ -456,7 +455,7 @@ QUnit.test("should not display subject when subject is the same as the thread na
await contains(".o-mail-Message:not(:contains(Salutations, voyageur))");
});
QUnit.test("scroll position is kept when navigating from one record to another", async (assert) => {
QUnit.test("scroll position is kept when navigating from one record to another", async () => {
patchUiSize({ size: SIZES.XXL });
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Harry Potter" });
@@ -464,48 +463,30 @@ QUnit.test("scroll position is kept when navigating from one record to another",
// Fill both channels with random messages in order for the scrollbar to
// appear.
pyEnv["mail.message"].create(
Array(40)
Array(50)
.fill(0)
.map((_, index) => ({
body: "Non Empty Body ".repeat(25),
model: "res.partner",
res_id: index & 1 ? partnerId_1 : partnerId_2,
res_id: index < 20 ? partnerId_1 : partnerId_2,
}))
);
const { openFormView } = await start();
openFormView("res.partner", partnerId_1);
await contains(".o_breadcrumb:contains(Harry Potter)");
await contains(".o-mail-Chatter");
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
const scrolltop_1 = $(".o-mail-Chatter")[0].scrollHeight / 2;
$(".o-mail-Chatter")[0].scrollTo({ top: scrolltop_1 });
await contains(".o-mail-Message", 20);
const scrollValue1 = $(".o-mail-Chatter")[0].scrollHeight / 2;
await contains(".o-mail-Chatter", 1, { scroll: 0 });
await scroll(".o-mail-Chatter", scrollValue1);
openFormView("res.partner", partnerId_2);
await contains(".o_breadcrumb:contains(Ron Weasley)");
/**
* The nextAnimationFrame is necessary because otherwise useAutoScroll would
* set the scroll to bottom after the manually set value from this test.
*/
await nextAnimationFrame();
const scrolltop_2 = $(".o-mail-Chatter")[0].scrollHeight / 3;
$(".o-mail-Chatter")[0].scrollTo({ top: scrolltop_2 });
await contains(".o-mail-Message", 30);
const scrollValue2 = $(".o-mail-Chatter")[0].scrollHeight / 3;
await scroll(".o-mail-Chatter", scrollValue2);
openFormView("res.partner", partnerId_1);
await contains(".o_breadcrumb:contains(Harry Potter)");
/**
* The nextAnimationFrame is necessary to give time for scroll to be restored.
*/
await nextAnimationFrame();
assert.ok(isScrolledTo($(".o-mail-Chatter")[0], scrolltop_1));
await contains(".o-mail-Message", 20);
await contains(".o-mail-Chatter", 1, { scroll: scrollValue1 });
openFormView("res.partner", partnerId_2);
await contains(".o_breadcrumb:contains(Ron Weasley)");
/**
* The nextAnimationFrame is necessary to give time for scroll to be restored.
*/
await nextAnimationFrame();
assert.ok(isScrolledTo($(".o-mail-Chatter")[0], scrolltop_2));
await contains(".o-mail-Message", 30);
await contains(".o-mail-Chatter", 1, { scroll: scrollValue2 });
});
QUnit.test("basic chatter rendering", async () => {
@@ -242,7 +242,7 @@ QUnit.test("Load 100 followers at once", async () => {
await contains(".o-mail-Followers-dropdown span:contains(Load more)", 0);
});
QUnit.test("Load 100 recipients at once", async (assert) => {
QUnit.test("Load 100 recipients at once", async () => {
const pyEnv = await startServer();
const partnerIds = pyEnv["res.partner"].create(
[...Array(210).keys()].map((i) => ({
@@ -1,11 +1,11 @@
/* @odoo-module */
import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size";
import { click, contains, start, startServer } from "@mail/../tests/helpers/test_utils";
import { click, contains, scroll, start, startServer } from "@mail/../tests/helpers/test_utils";
QUnit.module("Form renderer");
QUnit.test("Form view not scrolled when switching record", async (assert) => {
QUnit.test("Form view not scrolled when switching record", async () => {
const pyEnv = await startServer();
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{
@@ -16,8 +16,9 @@ QUnit.test("Form view not scrolled when switching record", async (assert) => {
]);
const messages = [...Array(60).keys()].map((id) => {
return {
body: "not empty",
model: "res.partner",
res_id: id % 2 ? partnerId_1 : partnerId_2,
res_id: id < 29 ? partnerId_1 : partnerId_2,
};
});
pyEnv["mail.message"].create(messages);
@@ -43,16 +44,16 @@ QUnit.test("Form view not scrolled when switching record", async (assert) => {
},
{ resIds: [partnerId_1, partnerId_2] }
);
await contains(".o_breadcrumb .active:contains(Partner 1)");
assert.strictEqual($(".o_content")[0].scrollTop, 0);
$(".o_content")[0].scrollTop = 150;
await contains(".o-mail-Message", 29);
await contains(".o_content", 1, { scroll: 0 });
await scroll(".o_content", 150);
await click(".o_pager_next");
await contains(".o_breadcrumb .active:contains(Partner 2)");
assert.strictEqual($(".o_content")[0].scrollTop, 0);
await contains(".o-mail-Message", 30);
await contains(".o_content", 1, { scroll: 150 });
await scroll(".o_content", 0);
await click(".o_pager_previous");
assert.strictEqual($(".o_content")[0].scrollTop, 0);
await contains(".o-mail-Message", 29);
await contains(".o_content", 1, { scroll: 0 });
});
QUnit.test(