diff --git a/addons/mail/static/src/utils/common/hooks.js b/addons/mail/static/src/utils/common/hooks.js index 4d3bdf48ee2..f021eb5ad4d 100644 --- a/addons/mail/static/src/utils/common/hooks.js +++ b/addons/mail/static/src/utils/common/hooks.js @@ -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); }); diff --git a/addons/mail/static/tests/chat_window/chat_window_tests.js b/addons/mail/static/tests/chat_window/chat_window_tests.js index 671af1526f6..3549b71a06e 100644 --- a/addons/mail/static/tests/chat_window/chat_window_tests.js +++ b/addons/mail/static/tests/chat_window/chat_window_tests.js @@ -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 }); diff --git a/addons/mail/static/tests/composer/composer_tests.js b/addons/mail/static/tests/composer/composer_tests.js index 028a0122d4b..24255a689e8 100644 --- a/addons/mail/static/tests/composer/composer_tests.js +++ b/addons/mail/static/tests/composer/composer_tests.js @@ -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 }); } ); diff --git a/addons/mail/static/tests/discuss_app/discuss_tests.js b/addons/mail/static/tests/discuss_app/discuss_tests.js index 99e2e96d5ee..bd26227784e 100644 --- a/addons/mail/static/tests/discuss_app/discuss_tests.js +++ b/addons/mail/static/tests/discuss_app/discuss_tests.js @@ -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 () => { diff --git a/addons/mail/static/tests/discuss_app/jump_to_present_tests.js b/addons/mail/static/tests/discuss_app/jump_to_present_tests.js index ab94845d161..d2391319439 100644 --- a/addons/mail/static/tests/discuss_app/jump_to_present_tests.js +++ b/addons/mail/static/tests/discuss_app/jump_to_present_tests.js @@ -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" }); }); diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index e1887556608..a6a783db1a1 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -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>} */ -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; } }); diff --git a/addons/mail/static/tests/message/message_tests.js b/addons/mail/static/tests/message/message_tests.js index a4e41ab178e..21042514cdd 100644 --- a/addons/mail/static/tests/message/message_tests.js +++ b/addons/mail/static/tests/message/message_tests.js @@ -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", diff --git a/addons/mail/static/tests/thread/thread_tests.js b/addons/mail/static/tests/thread/thread_tests.js index 9c82d1bb583..c892d63708d 100644 --- a/addons/mail/static/tests/thread/thread_tests.js +++ b/addons/mail/static/tests/thread/thread_tests.js @@ -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 }); diff --git a/addons/mail/static/tests/web/attachment_box_tests.js b/addons/mail/static/tests/web/attachment_box_tests.js index 16d3e0960a5..3be6051479b 100644 --- a/addons/mail/static/tests/web/attachment_box_tests.js +++ b/addons/mail/static/tests/web/attachment_box_tests.js @@ -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")); }); diff --git a/addons/mail/static/tests/web/chatter_tests.js b/addons/mail/static/tests/web/chatter_tests.js index cabfa4a6967..67d3bd86fd6 100644 --- a/addons/mail/static/tests/web/chatter_tests.js +++ b/addons/mail/static/tests/web/chatter_tests.js @@ -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 () => { diff --git a/addons/mail/static/tests/web/follower_list_menu_tests.js b/addons/mail/static/tests/web/follower_list_menu_tests.js index f06809af77f..16b61d4762f 100644 --- a/addons/mail/static/tests/web/follower_list_menu_tests.js +++ b/addons/mail/static/tests/web/follower_list_menu_tests.js @@ -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) => ({ diff --git a/addons/mail/static/tests/web/form_renderer_tests.js b/addons/mail/static/tests/web/form_renderer_tests.js index ccead229f9c..37401cfd7d2 100644 --- a/addons/mail/static/tests/web/form_renderer_tests.js +++ b/addons/mail/static/tests/web/form_renderer_tests.js @@ -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(