diff --git a/addons/base_iban/static/src/tests/iban_widget_test.js b/addons/base_iban/static/src/tests/iban_widget_test.js index 769fae6159c..d49476f14a1 100644 --- a/addons/base_iban/static/src/tests/iban_widget_test.js +++ b/addons/base_iban/static/src/tests/iban_widget_test.js @@ -1,27 +1,25 @@ -/** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils"; +/* @odoo-module */ + +import { startServer } from "@bus/../tests/helpers/mock_python_environment"; +import { click, contains, insertText } from "@bus/../tests/helpers/test_utils"; + +import { start } from "@mail/../tests/helpers/test_utils"; import { DELAY } from "@base_iban/components/iban_widget/iban_widget"; - -QUnit.module('Fields', {}, function () { +QUnit.module("Fields", {}, function () { QUnit.module("IbanWidget"); const [validIban, invalidIban] = ["BE12651194580992", "invalidIban!"]; - const openPreparedView = async (assert, validIbanList, startingAccNumber) => { - const target = getFixture(); + const openPreparedView = async () => { const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create([ + const partnerId = pyEnv["res.partner"].create([ { name: "Awesome partner", - bank_ids: [ - pyEnv['res.partner.bank'].create([{ acc_number: startingAccNumber }]), - ], + bank_ids: [pyEnv["res.partner.bank"].create([{ acc_number: "" }])], }, ]); const views = { - 'res.partner,false,form': - `
+ "res.partner,false,form": ` @@ -34,55 +32,50 @@ QUnit.module('Fields', {}, function () {
`, }; - const { advanceTime, afterNextRender, openView } = await start({ + const { advanceTime, openView } = await start({ serverData: { views }, hasTimeControl: true, mockRPC: function (route, args) { if (args.method === "check_iban") { - const iban = args.args[1].replace(/\s/g, ''); + const iban = args.args[1].replace(/\s/g, ""); return Promise.resolve(iban === validIban); } }, }); await openView({ res_id: partnerId, - res_model: 'res.partner', - views: [[false, 'form']], + res_model: "res.partner", + views: [[false, "form"]], }); - return { target, advanceTime, afterNextRender }; + return { advanceTime }; }; - QUnit.test('Iban Widget full flow', async assert => { - const { target, advanceTime, afterNextRender } = await openPreparedView(assert, [validIban], ""); - - assert.containsNone(target, ".o_iban", - "Shouldn't display any validation icon while not editing a specific line"); - - await click(target, "td.o_iban_cell"); - assert.containsNone(target, ".o_iban", "Shouldn't display any validation icon while iban is empty"); - - await editInput(target, ".o_iban_cell .o_input", invalidIban); - assert.containsNone(target, ".o_iban", "Shouldn't change its state of display before edition is finished"); - await afterNextRender(() => advanceTime(DELAY)); - assert.containsOnce(target, ".o_iban", "Should contain a validation icon 400ms after edition"); - assert.containsOnce(target, "i.fa.fa-times.o_iban_fail", "The validation icon should be the failed one"); - assert.containsNone(target, "i.fa.fa-check.o_iban", "The validation icon shouldn't be the successful one"); - - await clickSave(target); - assert.containsNone(target, ".o_iban", "Shouldn't display any validation while not editing"); - - await click(target, "td.o_iban_cell"); - await afterNextRender(() => advanceTime(DELAY)); - assert.containsOnce(target, "i.fa.fa-times.o_iban_fail", "The validation icon should be present while clicking on an already filled IBAN"); - - await editInput(target, ".o_iban_cell .o_input", validIban); - assert.containsOnce(target, "i.fa.fa-times.o_iban_fail", "The validation icon shouldn't change during the edition"); - await afterNextRender(() => advanceTime(DELAY)); - assert.containsOnce(target, ".o_iban", "Should contain a validation icon 400ms after edition"); - assert.containsOnce(target, "i.fa.fa-check.o_iban", "The validation icon should be the successful one"); - assert.containsNone(target, "i.fa.fa-times.o_iban_fail", "The validation icon shouldn't be the failed one"); - - await clickSave(target); - assert.containsNone(target, ".o_iban", "Shouldn't display any validation while not editing"); + QUnit.test("Iban Widget full flow [REQUIRE FOCUS]", async () => { + const { advanceTime } = await openPreparedView(); + await contains("td.o_iban_cell"); + await contains(".o_iban", { count: 0 }); // "Shouldn't display any validation icon while not editing a specific line" + await click("td.o_iban_cell"); + await contains(".o_iban_input_with_validator"); + await contains(".o_iban", { count: 0 }); // "Shouldn't display any validation icon while iban is empty" + await insertText(".o_iban_input_with_validator", invalidIban, { replace: true }); + await contains(".o_iban", { count: 0 }); // "Shouldn't change its state of display before edition is finished" + await advanceTime(DELAY); + await contains(".o_iban"); // "Should contain a validation icon 400ms after edition" + await contains("i.fa.fa-times.o_iban_fail"); // "The validation icon should be the failed one" + await contains("i.fa.fa-check.o_iban", { count: 0 }); // "The validation icon shouldn't be the successful one" + await click(".o_form_button_save"); + await contains(".o_iban", { count: 0 }); // "Shouldn't display any validation while not editing" + await click("td.o_iban_cell"); + await contains(".o_iban_input_with_validator"); + await advanceTime(DELAY); + await contains("i.fa.fa-times.o_iban_fail"); // "The validation icon should be present while clicking on an already filled IBAN" + await insertText(".o_iban_cell .o_input", validIban, { replace: true }); + await contains("i.fa.fa-times.o_iban_fail"); // "The validation icon shouldn't change during the edition" + await advanceTime(DELAY); + await contains(".o_iban"); // "Should contain a validation icon 400ms after edition" + await contains("i.fa.fa-check.o_iban"); // "The validation icon should be the successful one" + await contains("i.fa.fa-times.o_iban_fail", { count: 0 }); // "The validation icon shouldn't be the failed one" + await click(".o_form_button_save"); + await contains(".o_iban", { count: 0 }); // "Shouldn't display any validation while not editing" }); }); diff --git a/addons/bus/static/tests/assets_watchdog_tests.js b/addons/bus/static/tests/assets_watchdog_tests.js index 6f1adebc887..c0e17200021 100644 --- a/addons/bus/static/tests/assets_watchdog_tests.js +++ b/addons/bus/static/tests/assets_watchdog_tests.js @@ -1,21 +1,21 @@ -/** @odoo-module */ +/* @odoo-module */ import { busService } from "@bus/services/bus_service"; import { busParametersService } from "@bus/bus_parameters_service"; import { presenceService } from "@bus/services/presence_service"; import { multiTabService } from "@bus/multi_tab_service"; +import { click, contains } from "@bus/../tests/helpers/test_utils"; import { getPyEnv } from "@bus/../tests/helpers/mock_python_environment"; import { createWebClient } from "@web/../tests/webclient/helpers"; import { assetsWatchdogService } from "@bus/services/assets_watchdog_service"; -import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; const serviceRegistry = registry.category("services"); QUnit.module("Bus Assets WatchDog", (hooks) => { - let target; hooks.beforeEach((assert) => { serviceRegistry.add("assetsWatchdog", assetsWatchdogService); serviceRegistry.add("bus_service", busService); @@ -30,30 +30,16 @@ QUnit.module("Bus Assets WatchDog", (hooks) => { reload: () => assert.step("reloadPage"), }, }); - - target = getFixture(); }); QUnit.test("can listen on bus and displays notifications in DOM", async (assert) => { - assert.expect(4); - await createWebClient({}); const pyEnv = await getPyEnv(); - const { afterNextRender } = owl.App; - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone("broadcast", "bundle_changed", { - server_version: "NEW_MAJOR_VERSION", - }); + pyEnv["bus.bus"]._sendone("broadcast", "bundle_changed", { + server_version: "NEW_MAJOR_VERSION", }); - - assert.containsOnce(target, ".o_notification_body"); - assert.strictEqual( - target.querySelector(".o_notification_body .o_notification_content").textContent, - "The page appears to be out of date." - ); - - // reload by clicking on the reload button - await click(target, ".o_notification_buttons .btn-primary"); + await contains(".o_notification_content", { text: "The page appears to be out of date." }); + await click(".o_notification_buttons .btn-primary", { text: "Refresh" }); assert.verifySteps(["reloadPage"]); }); }); diff --git a/addons/bus/static/tests/helpers/test_utils.js b/addons/bus/static/tests/helpers/test_utils.js new file mode 100644 index 00000000000..aaefec08fa4 --- /dev/null +++ b/addons/bus/static/tests/helpers/test_utils.js @@ -0,0 +1,201 @@ +/* @odoo-module */ + +import { registerCleanup } from "@web/../tests/helpers/cleanup"; +import { getFixture } from "@web/../tests/helpers/utils"; + +/** + * Waits until exactly one element matching the given `selector` is present in + * `options.target` and then clicks on it. + * + * @param {string} selector + * @param {Object} [options={}] forwarded to `contains` + */ +export async function click(selector, options) { + await contains(selector, { click: true, ...options }); +} + +/** + * Waits until exactly one element matching the given `selector` is present in + * `options.target` and then inserts the given `content`. + * + * @param {string} selector + * @param {string} content + * @param {Object} [options = {}] + * @param {boolean} [options.replace=false] + */ +export async function insertText(selector, content, options = {}) { + const { replace = false } = options; + delete options.replace; + const [target] = await contains(selector, options); + target.focus(); + if (replace) { + target.value = ""; + target.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Backspace" })); + target.dispatchEvent(new window.KeyboardEvent("keyup", { key: "Backspace" })); + target.dispatchEvent(new window.InputEvent("input")); + target.dispatchEvent(new window.InputEvent("change")); + } + for (const char of content) { + target.value += char; + target.dispatchEvent(new window.KeyboardEvent("keydown", { key: char })); + target.dispatchEvent(new window.KeyboardEvent("keyup", { key: char })); + target.dispatchEvent(new window.InputEvent("input")); + target.dispatchEvent(new window.InputEvent("change")); + } +} + +/** + * 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={}] forwarded to `contains` + */ +export async function scroll(selector, scrollTop, options) { + await contains(selector, { setScroll: scrollTop, ...options }); +} + +let hasUsedContainsPositively = false; +QUnit.testStart(() => (hasUsedContainsPositively = false)); +/** + * Waits until `count` elements matching the given `selector` are present in + * `options.target`. + * + * @param {string} selector + * @param {Object} [options={}] + * @param {boolean} [options.click] if provided, clicks on the found element + * @param {number} [count=1] + * @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=getFixture()] + * @param {string} [options.text] if provided, the textContent of the found element(s) must match. + * @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, + { click, count = 1, scroll, setScroll, target = getFixture(), text, value } = {} +) { + if (count) { + hasUsedContainsPositively = true; + } else if (!hasUsedContainsPositively) { + throw new Error( + `Starting a test with "contains" of count 0 for selector "${selector}" is useless because it might immediately resolve. Start the test by checking that an expected element actually exists.` + ); + } + return new Promise((resolve, reject) => { + const scrollListeners = new Set(); + let selectorMessage = `${count} of "${selector}"`; + if (text !== undefined) { + selectorMessage = `${selectorMessage} with text "${text}"`; + } + 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"); + return; + } + let done = false; + const timer = setTimeout(() => { + clean(); + const res = select(); + const message = `Waited 5 second for ${selectorMessage}. Found ${res.length} instead.`; + QUnit.assert.ok(false, message); + reject(new Error(message)); + }, 5000); + const observer = new MutationObserver(() => { + const res = select(); + if (res.length === count) { + clean(); + execute(res, "after mutations"); + } + }); + observer.observe(document.body, { + attributes: true, + childList: true, + subtree: true, + }); + registerCleanup(() => { + if (!done) { + clean(); + const res = select(); + const message = `Test ended while waiting for ${selectorMessage}. Found ${res.length} instead.`; + QUnit.assert.ok(false, message); + reject(new Error(message)); + } + }); + function onScroll(ev) { + const res = select(); + if (res.length === count) { + clean(); + execute(res, "after scroll"); + } + } + function select() { + /** @type HTMLElement[] */ + let res; + try { + res = [...target.querySelectorAll(selector)]; + } catch (error) { + if (error.message.includes("Failed to execute 'querySelectorAll'")) { + // keep jquery for backwards compatibility until all tests are converted + res = [...$(target).find(selector)]; + } else { + throw error; + } + } + const filteredRes = res.filter( + (el) => + (text === undefined || el.textContent.trim() === text) && + (value === undefined || el.value === value) && + (scroll === undefined || + (scroll === "bottom" + ? Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) <= 1 + : Math.abs(el.scrollTop - scroll) <= 1)) + ); + 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})`; + if (click) { + 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/bus/static/tests/simple_notification_tests.js b/addons/bus/static/tests/simple_notification_tests.js index 5a809d990f5..ab8aae6fa15 100644 --- a/addons/bus/static/tests/simple_notification_tests.js +++ b/addons/bus/static/tests/simple_notification_tests.js @@ -5,6 +5,7 @@ import { busParametersService } from "@bus/bus_parameters_service"; import { multiTabService } from "@bus/multi_tab_service"; import { simpleNotificationService } from "@bus/simple_notification_service"; import { getPyEnv } from "@bus/../tests/helpers/mock_python_environment"; +import { contains } from "@bus/../tests/helpers/test_utils"; import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; @@ -22,62 +23,50 @@ QUnit.module("simple_notification", { }, }); -QUnit.test("receive and display simple notification with message", async (assert) => { +QUnit.test("receive and display simple notification with message", async () => { await createWebClient({}); const pyEnv = await getPyEnv(); - const { afterNextRender } = owl.App; - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { - message: "simple notification", - }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", }); - assert.strictEqual($(".o_notification_content").text(), "simple notification"); + await contains(".o_notification_content", { text: "simple notification" }); }); -QUnit.test("receive and display simple notification with title", async (assert) => { +QUnit.test("receive and display simple notification with title", async () => { await createWebClient({}); const pyEnv = await getPyEnv(); - const { afterNextRender } = owl.App; - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { - message: "simple notification", - title: "simple title", - }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", + title: "simple title", }); - assert.strictEqual($(".o_notification_title").text(), "simple title"); + await contains(".o_notification_title", { text: "simple title" }); }); -QUnit.test("receive and display simple notification with specific type", async (assert) => { +QUnit.test("receive and display simple notification with specific type", async () => { await createWebClient({}); const pyEnv = await getPyEnv(); - const { afterNextRender } = owl.App; - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { - message: "simple notification", - type: "info", - }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", + type: "info", }); - assert.containsOnce($, ".o_notification.border-info"); + await contains(".o_notification.border-info"); }); -QUnit.test("receive and display simple notification as sticky", async (assert) => { +QUnit.test("receive and display simple notification as sticky", async () => { await createWebClient({}); const pyEnv = await getPyEnv(); - const { afterNextRender } = owl.App; patchWithCleanup(browser, { setTimeout(fn) { /** - * Sticky notifications are removed after a delay. If the notification is still + * Non-sticky notifications are removed after a delay. If thenotification is still * present when this delay is set to 0 it means it is a sticky one. */ return super.setTimeout(fn, 0); }, }); - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { - message: "simple notification", - sticky: true, - }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "simple_notification", { + message: "simple notification", + sticky: true, }); - assert.containsOnce($, ".o_notification"); + await contains(".o_notification"); }); diff --git a/addons/calendar/static/tests/calendar_notification_tests.js b/addons/calendar/static/tests/calendar_notification_tests.js index 2aaf4e49837..08e8a5d65eb 100644 --- a/addons/calendar/static/tests/calendar_notification_tests.js +++ b/addons/calendar/static/tests/calendar_notification_tests.js @@ -1,30 +1,17 @@ -/** @odoo-module */ +/* @odoo-module */ -import { busService } from "@bus/services/bus_service"; -import { presenceService } from "@bus/services/presence_service"; -import { busParametersService } from "@bus/bus_parameters_service"; -import { multiTabService } from "@bus/multi_tab_service"; -import { getPyEnv } from '@bus/../tests/helpers/mock_python_environment'; +import { click, contains, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { createWebClient } from "@web/../tests/webclient/helpers"; import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service"; -import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; const serviceRegistry = registry.category("services"); -const { afterNextRender } = owl.App; QUnit.module("Calendar Notification", (hooks) => { - let target; hooks.beforeEach(() => { - target = getFixture(); - serviceRegistry.add("calendarNotification", calendarNotificationService); - serviceRegistry.add("bus_service", busService); - serviceRegistry.add("bus.parameters", busParametersService); - serviceRegistry.add("presence", presenceService); - serviceRegistry.add("multi_tab", multiTabService); patchWithCleanup(browser, { setTimeout(fn) { super.setTimeout(fn, 0); @@ -35,8 +22,7 @@ QUnit.module("Calendar Notification", (hooks) => { QUnit.test( "can listen on bus and display notifications in DOM and click OK", async (assert) => { - assert.expect(5); - + const pyEnv = await startServer(); const mockRPC = (route, args) => { if (route === "/calendar/notify") { return Promise.resolve([]); @@ -46,43 +32,33 @@ QUnit.module("Calendar Notification", (hooks) => { return Promise.resolve(true); } }; - await createWebClient({ mockRPC }); - const pyEnv = await getPyEnv(); - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, "calendar.alarm", [{ - "alarm_id": 1, - "event_id": 2, - "title": "Meeting", - "message": "Very old meeting message", - "timer": 20 * 60, - "notify_at": "1978-04-14 12:45:00", - }]); - }); - - assert.containsOnce(target, ".o_notification_body"); - assert.strictEqual( - target.querySelector(".o_notification_body .o_notification_content") - .textContent, - "Very old meeting message" - ); - - await click(target.querySelector(".o_notification_buttons .btn")); + await start({ mockRPC }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "calendar.alarm", [ + { + alarm_id: 1, + event_id: 2, + title: "Meeting", + message: "Very old meeting message", + timer: 20 * 60, + notify_at: "1978-04-14 12:45:00", + }, + ]); + await contains(".o_notification_content", { text: "Very old meeting message" }); + await click(".o_notification_buttons button", { text: "OK" }); + await contains(".o_notification", { count: 0 }); assert.verifySteps(["notifyAck"]); - assert.containsNone(target, ".o_notification"); } ); QUnit.test( "can listen on bus and display notifications in DOM and click Detail", async (assert) => { - assert.expect(5); - + const pyEnv = await startServer(); const mockRPC = (route, args) => { if (route === "/calendar/notify") { return Promise.resolve([]); } }; - const fakeActionService = { name: "action", start() { @@ -98,38 +74,28 @@ QUnit.module("Calendar Notification", (hooks) => { }, }; serviceRegistry.add("action", fakeActionService, { force: true }); - - await createWebClient({ mockRPC }); - const pyEnv = await getPyEnv(); - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, "calendar.alarm", [{ - "alarm_id": 1, - "event_id": 2, - "title": "Meeting", - "message": "Very old meeting message", - "timer": 20 * 60, - "notify_at": "1978-04-14 12:45:00", - }]); - }); - - assert.containsOnce(target, ".o_notification_body"); - assert.strictEqual( - target.querySelector(".o_notification_body .o_notification_content") - .textContent, - "Very old meeting message" - ); - - await click(target.querySelectorAll(".o_notification_buttons .btn")[1]); + await start({ mockRPC }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "calendar.alarm", [ + { + alarm_id: 1, + event_id: 2, + title: "Meeting", + message: "Very old meeting message", + timer: 20 * 60, + notify_at: "1978-04-14 12:45:00", + }, + ]); + await contains(".o_notification_content", { text: "Very old meeting message" }); + await click(".o_notification_buttons button", { text: "Details" }); + await contains(".o_notification", { count: 0 }); assert.verifySteps(["ir.actions.act_window"]); - assert.containsNone(target, ".o_notification"); } ); QUnit.test( "can listen on bus and display notifications in DOM and click Snooze", async (assert) => { - assert.expect(4); - + const pyEnv = await startServer(); const mockRPC = (route, args) => { if (route === "/calendar/notify") { return Promise.resolve([]); @@ -139,30 +105,21 @@ QUnit.module("Calendar Notification", (hooks) => { return Promise.resolve(true); } }; - - await createWebClient({ mockRPC }); - const pyEnv = await getPyEnv(); - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, "calendar.alarm", [{ - "alarm_id": 1, - "event_id": 2, - "title": "Meeting", - "message": "Very old meeting message", - "timer": 20 * 60, - "notify_at": "1978-04-14 12:45:00", - }]); - }); - - assert.containsOnce(target, ".o_notification_body"); - assert.strictEqual( - target.querySelector(".o_notification_body .o_notification_content") - .textContent, - "Very old meeting message" - ); - - await click(target.querySelectorAll(".o_notification_buttons .btn")[2]); + await start({ mockRPC }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "calendar.alarm", [ + { + alarm_id: 1, + event_id: 2, + title: "Meeting", + message: "Very old meeting message", + timer: 20 * 60, + notify_at: "1978-04-14 12:45:00", + }, + ]); + await contains(".o_notification_content", { text: "Very old meeting message" }); + await click(".o_notification_buttons button", { text: "Snooze" }); + await contains(".o_notification", { count: 0 }); assert.verifySteps([], "should only close the notification withtout calling a rpc"); - assert.containsNone(target, ".o_notification"); } ); }); diff --git a/addons/crm_livechat/static/tests/composer_tests.js b/addons/crm_livechat/static/tests/composer_tests.js index 2ca02bf30fc..f10fd402c1f 100644 --- a/addons/crm_livechat/static/tests/composer_tests.js +++ b/addons/crm_livechat/static/tests/composer_tests.js @@ -1,7 +1,6 @@ -/** @odoo-module */ +/* @odoo-module */ -import { afterNextRender, insertText, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { triggerHotkey } from "@web/../tests/helpers/utils"; +import { click, insertText, start, startServer } from "@mail/../tests/helpers/test_utils"; QUnit.module("composer"); @@ -19,8 +18,6 @@ QUnit.test("Can execute lead command", async function (assert) { }); await openDiscuss(channelId); await insertText(".o-mail-Composer-input", "/lead great lead"); - await afterNextRender(() => { - triggerHotkey("Enter"); - }); + await click(".o-mail-Composer-send:not(:disabled)"); assert.verifySteps(["execute_command_lead"]); }); diff --git a/addons/hr/static/tests/m2x_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js index 5d8233b84b0..381e1b13c0f 100644 --- a/addons/hr/static/tests/m2x_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -1,6 +1,6 @@ /* @odoo-module */ -import { afterNextRender, contains, start, startServer } from "@mail/../tests/helpers/test_utils"; +import { contains, start, startServer } from "@mail/../tests/helpers/test_utils"; import { dom } from "@web/../tests/legacy/helpers/test_utils"; @@ -58,38 +58,32 @@ QUnit.test("many2one_avatar_employee widget in list view", async function (asser ); // click on first employee - await afterNextRender(() => - dom.click(document.querySelector(".o_data_cell .o_m2o_avatar > img")) - ); + dom.click(document.querySelector(".o_data_cell .o_m2o_avatar > img")); + await contains(".o-mail-ChatWindow-name"); assert.verifySteps([`read hr.employee.public ${employeeId_1}`]); - assert.containsOnce(document.body, ".o-mail-ChatWindow-name"); assert.strictEqual(document.querySelector(".o-mail-ChatWindow-name").textContent, "Mario"); // click on second employee - await afterNextRender(() => - dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[1]) - ); + dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[1]); + await contains(".o-mail-ChatWindow-name", { count: 2 }); assert.verifySteps([`read hr.employee.public ${employeeId_2}`]); - assert.containsN(document.body, ".o-mail-ChatWindow-name", 2); assert.strictEqual( document.querySelectorAll(".o-mail-ChatWindow-name")[1].textContent, "Luigi" ); // click on third employee (same as first) - await afterNextRender(() => - dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[2]) - ); - assert.verifySteps( - [], - "employee should not have been read again because we already know its partner" - ); + dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[2]); assert.containsN( document.body, ".o-mail-ChatWindow-name", 2, "should still have only 2 chat windows because third is the same partner as first" ); + assert.verifySteps( + [], + "employee should not have been read again because we already know its partner" + ); }); QUnit.test("many2one_avatar_employee widget in kanban view", async function (assert) { @@ -121,7 +115,7 @@ QUnit.test("many2one_avatar_employee widget in kanban view", async function (ass views: [[false, "kanban"]], }); assert.strictEqual(document.querySelector(".o_kanban_record").innerText.trim(), ""); - assert.containsOnce(document.body, ".o_m2o_avatar"); + await contains(".o_m2o_avatar"); assert.strictEqual( document.querySelector(".o_m2o_avatar > img").getAttribute("data-src"), `/web/image/hr.employee.public/${employeeId}/avatar_128` @@ -226,7 +220,7 @@ QUnit.test("many2many_avatar_employee widget in form view", async function (asse `read hr.employee.public ${employeeId_1}`, `read hr.employee.public ${employeeId_2}`, ]); - assert.containsN(document.body, ".o-mail-ChatWindow-name", 2); + await contains(".o-mail-ChatWindow-name", { count: 2 }); }); QUnit.test("many2many_avatar_employee widget in list view", async function (assert) { @@ -270,17 +264,15 @@ QUnit.test("many2many_avatar_employee widget in list view", async function (asse ); // click on first employee badge - await afterNextRender(() => dom.click(document.querySelector(".o_data_cell .o_m2m_avatar"))); + dom.click(document.querySelector(".o_data_cell .o_m2m_avatar")); + await contains(".o-mail-ChatWindow-name"); assert.verifySteps([`read hr.employee.public ${employeeId_1}`]); - assert.containsOnce(document.body, ".o-mail-ChatWindow-name"); assert.strictEqual(document.querySelector(".o-mail-ChatWindow-name").textContent, "Mario"); // click on second employee - await afterNextRender(() => - dom.click(document.querySelectorAll(".o_data_cell .o_m2m_avatar")[1]) - ); + dom.click(document.querySelectorAll(".o_data_cell .o_m2m_avatar")[1]); + await contains(".o-mail-ChatWindow-name", { count: 2 }); assert.verifySteps([`read hr.employee.public ${employeeId_2}`]); - assert.containsN(document.body, ".o-mail-ChatWindow-name", 2); assert.strictEqual( document.querySelectorAll(".o-mail-ChatWindow-name")[1].textContent, "Yoshi" @@ -392,7 +384,7 @@ QUnit.test( res_id: avatarId, views: [[false, "form"]], }); - assert.containsN(document.body, ".o_field_many2many_avatar_employee .o_tag", 2); + await contains(".o_field_many2many_avatar_employee .o_tag", { count: 2 }); assert.strictEqual( document .querySelector(".o_field_many2many_avatar_employee .o_tag img") @@ -413,6 +405,6 @@ QUnit.test( `read hr.employee.public ${employeeId_1}`, `read hr.employee.public ${employeeId_2}`, ]); - assert.containsOnce(document.body, ".o-mail-ChatWindow-name"); + await contains(".o-mail-ChatWindow-name"); } ); diff --git a/addons/im_livechat/static/tests/composer_patch_tests.js b/addons/im_livechat/static/tests/composer_patch_tests.js index 912e8b3a221..7ad966d67e7 100644 --- a/addons/im_livechat/static/tests/composer_patch_tests.js +++ b/addons/im_livechat/static/tests/composer_patch_tests.js @@ -3,7 +3,6 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; import { - afterNextRender, click, contains, dragenterFiles, @@ -11,11 +10,9 @@ import { start, } from "@mail/../tests/helpers/test_utils"; -import { nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; - QUnit.module("composer (patch)"); -QUnit.test("No add attachments button", async (assert) => { +QUnit.test("No add attachments button", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "Livechat 1", @@ -27,9 +24,7 @@ QUnit.test("No add attachments button", async (assert) => { await contains("button[title='Attach files']", { count: 0 }); }); -QUnit.test("Attachment upload via drag and drop disabled", async (assert) => { - assert.expect(2); - +QUnit.test("Attachment upload via drag and drop disabled", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "Livechat 1", @@ -39,13 +34,13 @@ QUnit.test("Attachment upload via drag and drop disabled", async (assert) => { await openDiscuss(channelId); await contains(".o-mail-Composer"); dragenterFiles($(".o-mail-Composer-input")[0]); - await nextTick(); + // weak test: no guarantee that we waited long enough for the potential dropzone to show await contains(".o-mail-Dropzone", { count: 0 }); }); QUnit.test("Can execute help command on livechat channels", async (assert) => { const pyEnv = await startServer(); - pyEnv["discuss.channel"].create({ + const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 11", channel_member_ids: [ [0, 0, { partner_id: pyEnv.currentPartnerId }], @@ -54,7 +49,7 @@ QUnit.test("Can execute help command on livechat channels", async (assert) => { channel_type: "livechat", livechat_operator_id: pyEnv.currentPartnerId, }); - await start({ + const { openDiscuss } = await start({ mockRPC(route, args, originalMockRPC) { if (args.method === "execute_command_help") { assert.step("execute_command_help"); @@ -63,14 +58,13 @@ QUnit.test("Can execute help command on livechat channels", async (assert) => { return originalMockRPC(route, args); }, }); - await click(".o_menu_systray i[aria-label='Messages']"); - await click(".o-mail-NotificationItem"); + await openDiscuss(channelId); await insertText(".o-mail-Composer-input", "/help"); - triggerHotkey("Enter"); + await click(".o-mail-Composer-send:not(:disabled)"); assert.verifySteps(["execute_command_help"]); }); -QUnit.test('Receives visitor typing status "is typing"', async (assert) => { +QUnit.test('Receives visitor typing status "is typing"', async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 20", @@ -86,13 +80,11 @@ QUnit.test('Receives visitor typing status "is typing"', async (assert) => { await contains(".o-discuss-Typing", { text: "" }); const channel = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]])[0]; // simulate receive typing notification from livechat visitor "is typing" - await afterNextRender(() => - pyEnv.withUser(pyEnv.publicUserId, () => - env.services.rpc("/im_livechat/notify_typing", { - is_typing: true, - uuid: channel.uuid, - }) - ) + pyEnv.withUser(pyEnv.publicUserId, () => + env.services.rpc("/im_livechat/notify_typing", { + is_typing: true, + uuid: channel.uuid, + }) ); await contains(".o-discuss-Typing", { text: "Visitor 20 is typing..." }); }); @@ -120,7 +112,7 @@ QUnit.test('display canned response suggestions on typing ":"', async () => { await contains(".o-mail-Composer-suggestionList .o-open"); }); -QUnit.test("use a canned response", async (assert) => { +QUnit.test("use a canned response", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Mario", @@ -145,7 +137,7 @@ QUnit.test("use a canned response", async (assert) => { await contains(".o-mail-Composer-input", { value: "Hello! How are you? " }); }); -QUnit.test("use a canned response some text", async (assert) => { +QUnit.test("use a canned response some text", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Mario", @@ -163,9 +155,9 @@ QUnit.test("use a canned response some text", async (assert) => { const { openDiscuss } = await start(); await openDiscuss(channelId); await contains(".o-mail-Composer-suggestionList"); - assert.strictEqual($(".o-mail-Composer-input").val(), ""); + await contains(".o-mail-Composer-input", { value: "" }); await insertText(".o-mail-Composer-input", "bluhbluh "); - assert.strictEqual($(".o-mail-Composer-input").val(), "bluhbluh "); + await contains(".o-mail-Composer-input", { value: "bluhbluh " }); await insertText(".o-mail-Composer-input", ":"); await click(".o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "bluhbluh Hello! How are you? " }); diff --git a/addons/im_livechat/static/tests/discuss_patch_tests.js b/addons/im_livechat/static/tests/discuss_patch_tests.js index c7aa5c80b8f..2548eaa95b2 100644 --- a/addons/im_livechat/static/tests/discuss_patch_tests.js +++ b/addons/im_livechat/static/tests/discuss_patch_tests.js @@ -1,18 +1,13 @@ /* @odoo-module */ +import { click, contains, insertText } from "@bus/../tests/helpers/test_utils"; + import { Command } from "@mail/../tests/helpers/command"; -import { - afterNextRender, - click, - contains, - insertText, - start, - startServer, -} from "@mail/../tests/helpers/test_utils"; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; QUnit.module("discuss (patch)"); -QUnit.test("No call buttons", async (assert) => { +QUnit.test("No call buttons", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 11", @@ -25,11 +20,12 @@ QUnit.test("No call buttons", async (assert) => { }); const { openDiscuss } = await start(); await openDiscuss(); - assert.containsNone($, ".o-mail-Discuss-header button[title='Start a Call']"); - assert.containsNone($, ".o-mail-Discuss-header button[title='Show Call Settings']"); + await contains(".o-mail-Discuss-header"); + await contains(".o-mail-Discuss-header button[title='Start a Call']", { count: 0 }); + await contains(".o-mail-Discuss-header button[title='Show Call Settings']", { count: 0 }); }); -QUnit.test("No reaction button", async (assert) => { +QUnit.test("No reaction button", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 11", @@ -48,10 +44,10 @@ QUnit.test("No reaction button", async (assert) => { const { openDiscuss } = await start(); await openDiscuss(channelId); await click(".o-mail-Message"); - assert.containsNone($, "[title='Add a Reaction']"); + await contains("[title='Add a Reaction']", { count: 0 }); }); -QUnit.test("No reply button", async (assert) => { +QUnit.test("No reply button", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 11", @@ -70,10 +66,10 @@ QUnit.test("No reply button", async (assert) => { const { openDiscuss } = await start(); await openDiscuss(channelId); await click(".o-mail-Message"); - assert.containsNone($, "[title='Reply']"); + await contains("[title='Reply']", { count: 0 }); }); -QUnit.test("add livechat in the sidebar on visitor sending first message", async (assert) => { +QUnit.test("add livechat in the sidebar on visitor sending first message", async () => { const pyEnv = await startServer(); pyEnv["res.users"].write([pyEnv.currentUserId], { im_status: "online" }); const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" }); @@ -93,29 +89,24 @@ QUnit.test("add livechat in the sidebar on visitor sending first message", async }); const { env, openDiscuss } = await start(); await openDiscuss(); - assert.containsNone($, ".o-mail-DiscussSidebarCategory-livechat"); + await contains(".o-mail-DiscussSidebar"); + await contains(".o-mail-DiscussSidebarCategory-livechat", { count: 0 }); // simulate livechat visitor sending a message const [channel] = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]]); - await afterNextRender(() => - pyEnv.withUser(pyEnv.publicUserId, () => - env.services.rpc("/im_livechat/chat_post", { - uuid: channel.uuid, - message_content: "new message", - }) - ) + pyEnv.withUser(pyEnv.publicUserId, () => + env.services.rpc("/im_livechat/chat_post", { + uuid: channel.uuid, + message_content: "new message", + }) ); - assert.containsOnce($, ".o-mail-DiscussSidebarCategory-livechat"); - assert.containsOnce( - $, - ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel" - ); - assert.containsOnce( - $, + await contains(".o-mail-DiscussSidebarCategory-livechat"); + await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel"); + await contains( ".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel:contains(Visitor (Belgium))" ); }); -QUnit.test("reaction button should not be present on livechat", async (assert) => { +QUnit.test("reaction button should not be present on livechat", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 11", @@ -131,10 +122,10 @@ QUnit.test("reaction button should not be present on livechat", async (assert) = await insertText(".o-mail-Composer-input", "Test"); await click(".o-mail-Composer-send:not(:disabled)"); await click(".o-mail-Message"); - assert.containsNone($, "[title='Add a Reaction']"); + await contains("[title='Add a Reaction']", { count: 0 }); }); -QUnit.test("invite button should be present on livechat", async (assert) => { +QUnit.test("invite button should be present on livechat", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 11", @@ -147,12 +138,12 @@ QUnit.test("invite button should be present on livechat", async (assert) => { }); const { openDiscuss } = await start(); await openDiscuss(channelId); - assert.containsOnce($, ".o-mail-Discuss button[title='Add Users']"); + await contains(".o-mail-Discuss button[title='Add Users']"); }); QUnit.test( "livechats are sorted by last activity time in the sidebar: most recent at the top", - async (assert) => { + async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create([ { diff --git a/addons/im_livechat/static/tests/embed/history_command_tests.js b/addons/im_livechat/static/tests/embed/history_command_tests.js index 883b6cb332f..f09b942ea26 100644 --- a/addons/im_livechat/static/tests/embed/history_command_tests.js +++ b/addons/im_livechat/static/tests/embed/history_command_tests.js @@ -2,7 +2,7 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; -import { afterNextRender, click, insertText } from "@mail/../tests/helpers/test_utils"; +import { click, contains, insertText } from "@mail/../tests/helpers/test_utils"; import { loadDefaultConfig, start } from "@im_livechat/../tests/embed/helper/test_utils"; import { nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; @@ -21,7 +21,8 @@ QUnit.test("Handle livechat history command", async (assert) => { }); await click(".o-livechat-LivechatButton"); await insertText(".o-mail-Composer-input", "Hello World!"); - await afterNextRender(() => triggerHotkey("Enter")); + triggerHotkey("Enter"); + await contains(".o-mail-Message", { count: 2 }); const thread = env.services["im_livechat.livechat"].thread; pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "im_livechat.history_command", { id: thread.id, diff --git a/addons/im_livechat/static/tests/go_to_oldest_unread_thread_tests.js b/addons/im_livechat/static/tests/go_to_oldest_unread_thread_tests.js index bffeb5fc3e2..3b379eb19ab 100644 --- a/addons/im_livechat/static/tests/go_to_oldest_unread_thread_tests.js +++ b/addons/im_livechat/static/tests/go_to_oldest_unread_thread_tests.js @@ -1,16 +1,17 @@ /* @odoo-module */ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; +import { insertText, contains } from "@bus/../tests/helpers/test_utils"; import { Command } from "@mail/../tests/helpers/command"; import { nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; import { patchUiSize } from "@mail/../tests/helpers/patch_ui_size"; -import { afterNextRender, insertText, start } from "@mail/../tests/helpers/test_utils"; +import { start } from "@mail/../tests/helpers/test_utils"; QUnit.module("go to oldest unread livechat"); -QUnit.test("tab on discuss composer goes to oldest unread livechat", async (assert) => { +QUnit.test("tab on discuss composer goes to oldest unread livechat", async () => { const pyEnv = await startServer(); const channelIds = pyEnv["discuss.channel"].create([ { @@ -70,18 +71,16 @@ QUnit.test("tab on discuss composer goes to oldest unread livechat", async (asse const { openDiscuss } = await start(); await openDiscuss(channelIds[0]); - assert.containsOnce($, ".o-mail-DiscussSidebarChannel.o-active:contains(Visitor 11)"); - assert.containsOnce($, ".o-mail-Composer-footer:contains(Tab to next livechat)"); - await afterNextRender(() => { - document.querySelector(".o-mail-Composer-input").focus(); - triggerHotkey("Tab"); - }); - assert.containsOnce($, ".o-mail-DiscussSidebarChannel.o-active:contains(Visitor 13)"); - await afterNextRender(() => { - document.querySelector(".o-mail-Composer-input").focus(); - triggerHotkey("Tab"); - }); - assert.containsOnce($, ".o-mail-DiscussSidebarChannel.o-active:contains(Visitor 12)"); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" }); + await contains(".o-mail-Composer-footer", { text: "Tab to next livechat" }); + document.querySelector(".o-mail-Composer-input").focus(); + await contains(".o-active .o-mail-DiscussSidebar-badge", { count: 0 }); + triggerHotkey("Tab"); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 13" }); + document.querySelector(".o-mail-Composer-input").focus(); + await contains(".o-active .o-mail-DiscussSidebar-badge", { count: 0 }); + triggerHotkey("Tab"); + await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 12" }); }); QUnit.test("switching to folded chat window unfolds it", async (assert) => { @@ -119,16 +118,10 @@ QUnit.test("switching to folded chat window unfolds it", async (assert) => { }, ]); await start(); - assert.containsOnce( - $, - ".o-mail-ChatWindow.o-folded .o-mail-ChatWindow-name:contains(Visitor 12)" - ); - await afterNextRender(() => { - $(".o-mail-ChatWindow:contains(Visitor 11) .o-mail-Composer-input").trigger("focus"); - triggerHotkey("Tab"); - }); - assert.containsOnce( - $, + await contains(".o-mail-ChatWindow.o-folded .o-mail-ChatWindow-name:contains(Visitor 12)"); + $(".o-mail-ChatWindow:contains(Visitor 11) .o-mail-Composer-input").trigger("focus"); + triggerHotkey("Tab"); + await contains( ".o-mail-ChatWindow:not(.o-folded) .o-mail-ChatWindow-name:contains(Visitor 12)" ); assert.strictEqual( @@ -180,12 +173,9 @@ QUnit.test("switching to hidden chat window unhides it", async (assert) => { $, ".o-mail-ChatWindow.o-folded .o-mail-ChatWindow-name:contains(Visitor 12)" ); - await afterNextRender(() => { - $(".o-mail-ChatWindow:contains(Visitor 11) .o-mail-Composer-input").trigger("focus"); - triggerHotkey("Tab"); - }); - assert.containsOnce( - $, + $(".o-mail-ChatWindow:contains(Visitor 11) .o-mail-Composer-input").trigger("focus"); + triggerHotkey("Tab"); + await contains( ".o-mail-ChatWindow:not(.o-folded) .o-mail-ChatWindow-name:contains(Visitor 12)" ); assert.strictEqual( @@ -194,7 +184,7 @@ QUnit.test("switching to hidden chat window unhides it", async (assert) => { ); }); -QUnit.test("tab on composer doesn't switch thread if user is typing", async (assert) => { +QUnit.test("tab on composer doesn't switch thread if user is typing", async () => { const pyEnv = await startServer(); const channelIds = pyEnv["discuss.channel"].create([ { @@ -228,7 +218,7 @@ QUnit.test("tab on composer doesn't switch thread if user is typing", async (ass await insertText(".o-mail-Composer-input", "Hello, "); triggerHotkey("Tab"); await nextTick(); - assert.containsOnce($, ".o-mail-DiscussSidebarChannel.o-active:contains(Visitor 11)"); + await contains(".o-mail-DiscussSidebarChannel.o-active:contains(Visitor 11)"); }); QUnit.test("tab on composer doesn't switch thread if no unread thread", async (assert) => { @@ -261,5 +251,5 @@ QUnit.test("tab on composer doesn't switch thread if no unread thread", async (a document.querySelector(".o-mail-Composer-input").focus(); triggerHotkey("Tab"); await nextTick(); - assert.containsOnce($, ".o-mail-DiscussSidebarChannel.o-active:contains(Visitor 11)"); + await contains(".o-mail-DiscussSidebarChannel.o-active:contains(Visitor 11)"); }); diff --git a/addons/im_livechat/static/tests/thread_icon_patch_tests.js b/addons/im_livechat/static/tests/thread_icon_patch_tests.js index 936dce8ba7f..c54c1918394 100644 --- a/addons/im_livechat/static/tests/thread_icon_patch_tests.js +++ b/addons/im_livechat/static/tests/thread_icon_patch_tests.js @@ -1,10 +1,10 @@ /* @odoo-module */ -import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; +import { contains, start, startServer } from "@mail/../tests/helpers/test_utils"; QUnit.module("thread icon (patch)"); -QUnit.test("Public website visitor is typing", async (assert) => { +QUnit.test("Public website visitor is typing", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ anonymous_name: "Visitor 20", @@ -17,20 +17,17 @@ QUnit.test("Public website visitor is typing", async (assert) => { }); const { env, openDiscuss } = await start(); await openDiscuss(channelId); - assert.containsOnce($, ".o-mail-ThreadIcon .fa.fa-comments"); + await contains(".o-mail-ThreadIcon .fa.fa-comments"); const channel = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]])[0]; // simulate receive typing notification from livechat visitor "is typing" - await afterNextRender(() => - pyEnv.withUser(pyEnv.publicUserId, () => - env.services.rpc("/im_livechat/notify_typing", { - is_typing: true, - uuid: channel.uuid, - }) - ) + pyEnv.withUser(pyEnv.publicUserId, () => + env.services.rpc("/im_livechat/notify_typing", { + is_typing: true, + uuid: channel.uuid, + }) ); - assert.containsOnce($, ".o-mail-Discuss-header .o-discuss-Typing-icon"); - assert.containsOnce( - $, + await contains(".o-mail-Discuss-header .o-discuss-Typing-icon"); + await contains( ".o-mail-Discuss-header .o-discuss-Typing-icon[title='Visitor 20 is typing...']" ); }); diff --git a/addons/mail/static/tests/composer/composer_tests.js b/addons/mail/static/tests/composer/composer_tests.js index bc23599e811..ffa4d07b22d 100644 --- a/addons/mail/static/tests/composer/composer_tests.js +++ b/addons/mail/static/tests/composer/composer_tests.js @@ -4,13 +4,13 @@ import { Composer } from "@mail/core/common/composer"; import { Command } from "@mail/../tests/helpers/command"; import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; import { - afterNextRender, click, contains, createFile, dragenterFiles, dropFiles, insertText, + nextAnimationFrame, pasteFiles, scroll, start, @@ -138,7 +138,8 @@ QUnit.test("Cursor is positioned after emoji after adding it", async (assert) => const channelId = pyEnv["discuss.channel"].create({ name: "pétanque-tournament-14" }); const { openDiscuss } = await start(); openDiscuss(channelId); - const textarea = (await insertText(".o-mail-Composer-input", "Blabla"))[0]; + await insertText(".o-mail-Composer-input", "Blabla"); + const [textarea] = await contains(".o-mail-Composer-input"); textarea.setSelectionRange(2, 2); await click("button[aria-label='Emojis']"); await click(".o-Emoji", { text: "🤠" }); @@ -882,12 +883,9 @@ QUnit.test("Message is sent only once when pressing enter twice in a row", async const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "Hello World!"); - await afterNextRender(async () => { - // weak test, no guarantee that we waited long enough for the potential second message to be - // posted. afterNextRender is the *minimum* to see the issue (before it was fixed): it - // should not be removed even it the test works without it. - triggerHotkey("Enter"); - triggerHotkey("Enter"); - }); + triggerHotkey("Enter"); + triggerHotkey("Enter"); + // weak test, no guarantee that we waited long enough for the potential second message to be posted + await nextAnimationFrame(); await contains(".o-mail-Message-content", { text: "Hello World!" }); }); diff --git a/addons/mail/static/tests/discuss/call/call_tests.js b/addons/mail/static/tests/discuss/call/call_tests.js index dac0bb176be..fa0da1fc1f7 100644 --- a/addons/mail/static/tests/discuss/call/call_tests.js +++ b/addons/mail/static/tests/discuss/call/call_tests.js @@ -2,16 +2,16 @@ import { Command } from "@mail/../tests/helpers/command"; import { - afterNextRender, click, contains, mockGetMedia, + nextAnimationFrame, start, startServer, } from "@mail/../tests/helpers/test_utils"; import { browser } from "@web/core/browser/browser"; -import { nextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; QUnit.module("call"); @@ -104,8 +104,8 @@ QUnit.test("should disconnect when closing page while in call", async (assert) = await click(".o-mail-Discuss-header button[title='Start a Call']"); await contains(".o-discuss-Call"); // simulate page close - await afterNextRender(() => window.dispatchEvent(new Event("pagehide"), { bubble: true })); - await nextTick(); + window.dispatchEvent(new Event("pagehide"), { bubble: true }); + await nextAnimationFrame(); assert.verifySteps(["sendBeacon_leave_call"]); }); diff --git a/addons/mail/static/tests/discuss/core/web/chat_window_new_message_tests.js b/addons/mail/static/tests/discuss/core/web/chat_window_new_message_tests.js index e01231c40f3..63f04ce7f12 100644 --- a/addons/mail/static/tests/discuss/core/web/chat_window_new_message_tests.js +++ b/addons/mail/static/tests/discuss/core/web/chat_window_new_message_tests.js @@ -131,10 +131,9 @@ QUnit.test( await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click("button", { text: "New Message" }); - await insertText(".o-discuss-ChannelSelector", "131"); + await insertText(".o-discuss-ChannelSelector input", "131"); await click(".o-discuss-ChannelSelector-suggestion a"); await contains(".o-mail-ChatWindow-name", { count: 0, text: "New message" }); - await contains(".o-mail-ChatWindow"); } ); @@ -146,6 +145,6 @@ QUnit.test("new message autocomplete should automatically select first result", await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click("button", { text: "New Message" }); - await insertText(".o-discuss-ChannelSelector", "131"); + await insertText(".o-discuss-ChannelSelector input", "131"); await contains(".o-discuss-ChannelSelector-suggestion a.o-mail-NavigableList-active"); }); diff --git a/addons/mail/static/tests/discuss/core/web/crosstab_tests.js b/addons/mail/static/tests/discuss/core/web/crosstab_tests.js index b6722a658aa..f60ad43fec9 100644 --- a/addons/mail/static/tests/discuss/core/web/crosstab_tests.js +++ b/addons/mail/static/tests/discuss/core/web/crosstab_tests.js @@ -17,7 +17,7 @@ QUnit.test("Channel subscription is renewed when channel is manually added", asy }); openDiscuss(); await click("[title='Add or join a channel']"); - await insertText(".o-discuss-ChannelSelector", "General"); + await insertText(".o-discuss-ChannelSelector input", "General"); await click(".o-discuss-ChannelSelector-suggestion:eq(0)"); await contains(".o-mail-DiscussSidebarChannel", { count: 1 }); await new Promise((resolve) => setTimeout(resolve)); // update of channels is debounced diff --git a/addons/mail/static/tests/discuss/core/web/discuss_tests.js b/addons/mail/static/tests/discuss/core/web/discuss_tests.js index 57919ee0712..6492aec4c03 100644 --- a/addons/mail/static/tests/discuss/core/web/discuss_tests.js +++ b/addons/mail/static/tests/discuss/core/web/discuss_tests.js @@ -142,7 +142,7 @@ QUnit.test("chat search should display no result when no matches found", async ( const { openDiscuss } = await start(); openDiscuss(); await click(".o-mail-DiscussSidebar i[title='Start a conversation']"); - await insertText(".o-discuss-ChannelSelector", "Rainbow Panda"); + await insertText(".o-discuss-ChannelSelector input", "Rainbow Panda"); await contains(".o-discuss-ChannelSelector-suggestion", { text: "No results found" }); }); @@ -153,7 +153,7 @@ QUnit.test("chat search should not be visible when clicking outside of the field const { openDiscuss } = await start(); openDiscuss(); await click(".o-mail-DiscussSidebar i[title='Start a conversation']"); - await insertText(".o-discuss-ChannelSelector", "Panda"); + await insertText(".o-discuss-ChannelSelector input", "Panda"); await contains(".o-discuss-ChannelSelector-suggestion"); await click(".o-mail-DiscussSidebar"); await contains(".o-discuss-ChannelSelector-suggestion", { count: 0 }); diff --git a/addons/mail/static/tests/discuss_app/discuss_tests.js b/addons/mail/static/tests/discuss_app/discuss_tests.js index 592cb9a7b5d..52ec5c49918 100644 --- a/addons/mail/static/tests/discuss_app/discuss_tests.js +++ b/addons/mail/static/tests/discuss_app/discuss_tests.js @@ -6,7 +6,6 @@ import { TEST_USER_IDS } from "@bus/../tests/helpers/test_constants"; import { Command } from "@mail/../tests/helpers/command"; import { patchUiSize } from "@mail/../tests/helpers/patch_ui_size"; import { - afterNextRender, click, contains, createFile, @@ -1692,6 +1691,8 @@ QUnit.test("restore thread scroll position", async () => { QUnit.test("Message shows up even if channel data is incomplete", async () => { const { env, openDiscuss, pyEnv } = await start(); openDiscuss(); + await contains(".o-mail-DiscussSidebarCategory-chat"); + await contains(".o-mail-DiscussSidebarChannel", { count: 0 }); const correspondentUserId = pyEnv["res.users"].create({ name: "Albert" }); const correspondentPartnerId = pyEnv["res.partner"].create({ name: "Albert", @@ -1717,21 +1718,14 @@ QUnit.test("Message shows up even if channel data is incomplete", async () => { channel_id: channelId, }) ); - await afterNextRender(() => - pyEnv.withUser(correspondentUserId, () => - env.services.rpc("/mail/message/post", { - post_data: { body: "hello world", message_type: "comment" }, - thread_id: channelId, - thread_model: "discuss.channel", - }) - ) - ); - await click( - ".o-mail-DiscussSidebarCategory-chat + .o-mail-DiscussSidebarChannel:contains(Albert)" - ); - await click( - ".o-mail-DiscussSidebarCategory-chat + .o-mail-DiscussSidebarChannel:contains(Albert)" + await pyEnv.withUser(correspondentUserId, () => + env.services.rpc("/mail/message/post", { + post_data: { body: "hello world", message_type: "comment" }, + thread_id: channelId, + thread_model: "discuss.channel", + }) ); + await click(".o-mail-DiscussSidebarChannel:contains(Albert)"); await contains(".o-mail-Message-content", { text: "hello world" }); }); diff --git a/addons/mail/static/tests/discuss_app/inbox_tests.js b/addons/mail/static/tests/discuss_app/inbox_tests.js index c28f7d55482..d7ee54fd30a 100644 --- a/addons/mail/static/tests/discuss_app/inbox_tests.js +++ b/addons/mail/static/tests/discuss_app/inbox_tests.js @@ -1,20 +1,15 @@ /* @odoo-module */ import { - afterNextRender, click, contains, insertText, + nextAnimationFrame, start, startServer, } from "@mail/../tests/helpers/test_utils"; -import { - patchWithCleanup, - triggerHotkey, - mockTimeout, - makeDeferred, -} from "@web/../tests/helpers/utils"; +import { patchWithCleanup, triggerHotkey, makeDeferred } from "@web/../tests/helpers/utils"; QUnit.module("discuss inbox"); @@ -619,6 +614,8 @@ QUnit.test("emptying inbox displays rainbow man in inbox", async () => { const { openDiscuss } = await start(); openDiscuss(); await click("button", { text: "Mark all read" }); + await contains("button:contains(Inbox) .badge", { count: 0 }); + await nextAnimationFrame(); await contains(".o_reward_rainbow"); }); @@ -647,7 +644,8 @@ QUnit.test("emptying inbox doesn't display rainbow man in another thread", async message_ids: [messageId], needaction_inbox_counter: 0, }); - await afterNextRender(() => mockTimeout().execRegisteredTimeouts); await contains("button:contains(Inbox) .badge", { count: 0 }); + await nextAnimationFrame(); + // weak test, no guarantee that we waited long enough for the potential rainbow man to show await contains(".o_reward_rainbow", { count: 0 }); }); diff --git a/addons/mail/static/tests/discuss_app/sidebar_tests.js b/addons/mail/static/tests/discuss_app/sidebar_tests.js index 1e398b0352b..5b2c4f1ad50 100644 --- a/addons/mail/static/tests/discuss_app/sidebar_tests.js +++ b/addons/mail/static/tests/discuss_app/sidebar_tests.js @@ -2,16 +2,15 @@ import { Command } from "@mail/../tests/helpers/command"; import { - afterNextRender, click, contains, insertText, + nextAnimationFrame, start, startServer, } from "@mail/../tests/helpers/test_utils"; import { getOrigin } from "@web/core/utils/urls"; -import { nextTick } from "@web/../tests/helpers/utils"; QUnit.module("discuss sidebar"); @@ -669,12 +668,10 @@ QUnit.test("channel - avatar: should update avatar url from bus", async (assert) `img[data-src='${getOrigin()}/discuss/channel/${channelId}/avatar_128?unique=101010']`, { count: 2 } ); - await afterNextRender(() => { - env.services.orm.call("discuss.channel", "write", [ - [channelId], - { image_128: "This field does not matter" }, - ]); - }); + await env.services.orm.call("discuss.channel", "write", [ + [channelId], + { image_128: "This field does not matter" }, + ]); const result = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]]); const newCacheKey = result[0]["avatarCacheKey"]; await contains( @@ -743,13 +740,11 @@ QUnit.test("channel - states: close from the bus", async () => { }); const { openDiscuss } = await start(); openDiscuss(); - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { - "res.users.settings": { - id: userSettingsId, - is_discuss_sidebar_category_channel_open: false, - }, - }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: userSettingsId, + is_discuss_sidebar_category_channel_open: false, + }, }); await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-right"); await contains("button", { count: 0, text: "channel1" }); @@ -764,13 +759,11 @@ QUnit.test("channel - states: open from the bus", async () => { }); const { openDiscuss } = await start(); openDiscuss(); - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { - "res.users.settings": { - id: userSettingsId, - is_discuss_sidebar_category_channel_open: true, - }, - }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: userSettingsId, + is_discuss_sidebar_category_channel_open: true, + }, }); await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-down"); await contains("button", { text: "channel1" }); @@ -890,13 +883,11 @@ QUnit.test("chat - states: open from the bus", async () => { }); const { openDiscuss } = await start(); openDiscuss(); - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { - "res.users.settings": { - id: userSettingsId, - is_discuss_sidebar_category_chat_open: true, - }, - }); + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: userSettingsId, + is_discuss_sidebar_category_chat_open: true, + }, }); await contains(".o-mail-DiscussSidebarCategory-chat .oi-chevron-down"); await contains("button", { text: "Mitchell Admin" }); @@ -1037,17 +1028,16 @@ QUnit.test("Do no channel_info after unpin", async (assert) => { }); openDiscuss(channelId); await click(".o-mail-DiscussSidebarChannel-commands [title='Unpin Conversation']"); - await afterNextRender(() => { - env.services.rpc("/mail/message/post", { - thread_id: channelId, - thread_model: "discuss.channel", - post_data: { - body: "Hello world", - message_type: "comment", - }, - }); + env.services.rpc("/mail/message/post", { + thread_id: channelId, + thread_model: "discuss.channel", + post_data: { + body: "Hello world", + message_type: "comment", + }, }); - await nextTick(); + await nextAnimationFrame(); + // weak test, no guarantee that we waited long enough for the potential rpc to be done assert.verifySteps([]); }); diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index 3121e1981bd..251621b039e 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -1,5 +1,6 @@ /* @odoo-module */ +import { click, contains, insertText, scroll } from "@bus/../tests/helpers/test_utils"; import { getPyEnv, startServer } from "@bus/../tests/helpers/mock_python_environment"; import { loadEmoji } from "@web/core/emoji_picker/emoji_picker"; @@ -354,68 +355,6 @@ function pasteFiles(el, files) { el.dispatchEvent(ev); } -//------------------------------------------------------------------------------ -// Public: input utilities -//------------------------------------------------------------------------------ - -/** - * @param {string} selector - * @param {string} content - * @param {Object} [param2 = {}] - * @param {boolean} [param2.replace = false] - */ -export async function insertText(target, content, { replace = false } = {}) { - if (typeof target === "string") { - target = (await contains(target))[0]; - } - if (replace) { - target.value = ""; - } - target.focus(); - for (const char of content) { - document.execCommand("insertText", false, char); - target.dispatchEvent(new window.KeyboardEvent("keydown", { key: char })); - target.dispatchEvent(new window.KeyboardEvent("keyup", { key: char })); - target.dispatchEvent(new window.InputEvent("input")); - target.dispatchEvent(new window.InputEvent("change")); - } - if (!content) { - target.dispatchEvent(new window.InputEvent("input")); - target.dispatchEvent(new window.InputEvent("change")); - } - return $(target); -} - -//------------------------------------------------------------------------------ -// Public: DOM utilities -//------------------------------------------------------------------------------ - -/** - * Determine if a DOM element has been totally scrolled - * - * A 1px margin of error is given to accomodate subpixel rounding issues and - * Element.scrollHeight value being either int or decimal - * - * @param {DOM.Element} el - * @returns {boolean} - */ -function isScrolledToBottom(el) { - return Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) <= 1; -} - -/** - * Determine if a DOM element is scrolled to the given scroll top position. - * - * A 1px margin of error is given to accomodate subpixel rounding issues and - * Element.scrollHeight value being either int or decimal - * - * @param {DOM.Element} el - * @param {number} scrollTop expected scroll top value. - * @returns {boolean} - */ -function isScrolledTo(el, scrollTop) { - return Math.abs(el.scrollTop - scrollTop) <= 1; -} //------------------------------------------------------------------------------ // Public: web API utilities //------------------------------------------------------------------------------ @@ -492,177 +431,15 @@ export function mockGetMedia() { //------------------------------------------------------------------------------ export { - afterNextRender, + click, + contains, dragenterFiles, dropFiles, + insertText, nextAnimationFrame, nextTick, pasteFiles, + scroll, start, startServer, }; - -/** - * Waits until exactly one element matching the given selector is present in - * `options.target` and then clicks on it. - * - * @param {string} selector - * @param {Object} [options={}] forwarded to `contains` - */ -export async function click(selector, options) { - await contains(selector, { click: true, ...options }); -} - -/** - * 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={}] forwarded to `contains` - */ -export async function scroll(selector, scrollTop, options) { - await contains(selector, { setScroll: scrollTop, ...options }); -} - -let hasUsedContainsPositively = false; -QUnit.testStart(() => (hasUsedContainsPositively = false)); -/** - * Waits until `count` elements matching the given selector are present in - * `options.target`. - * - * @param {string} selector - * @param {Object} [options={}] - * @param {boolean} [options.click] if provided, clicks on the found element - * @param {number} [count=1] - * @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=getFixture()] - * @param {string} [options.text] if provided, the textContent of the found element(s) must match. - * @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, - { click, count = 1, scroll, setScroll, target = getFixture(), text, value } = {} -) { - if (count) { - hasUsedContainsPositively = true; - } else if (!hasUsedContainsPositively) { - throw new Error( - `Starting a test with "contains" of count 0 for selector "${selector}" is useless because it might immediately resolve. Start the test by checking that an expected element actually exists.` - ); - } - return new Promise((resolve, reject) => { - const scrollListeners = new Set(); - let selectorMessage = `${count} of "${selector}"`; - if (text !== undefined) { - selectorMessage = `${selectorMessage} with text "${text}"`; - } - 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"); - return; - } - let done = false; - const timer = setTimeout(() => { - clean(); - const res = select(); - const message = `Waited 5 second for ${selectorMessage}. Found ${res.length} instead.`; - QUnit.assert.ok(false, message); - reject(new Error(message)); - }, 5000); - const observer = new MutationObserver(() => { - const res = select(); - if (res.length === count) { - clean(); - execute(res, "after mutations"); - } - }); - observer.observe(document.body, { - attributes: true, - childList: true, - subtree: true, - }); - registerCleanup(() => { - if (!done) { - clean(); - const res = select(); - const message = `Test ended while waiting for ${selectorMessage}. Found ${res.length} instead.`; - QUnit.assert.ok(false, message); - reject(new Error(message)); - } - }); - function onScroll(ev) { - const res = select(); - if (res.length === count) { - clean(); - execute(res, "after scroll"); - } - } - function select() { - /** @type HTMLElement[] */ - let res; - try { - res = [...target.querySelectorAll(selector)]; - } catch (error) { - if (error.message.includes("Failed to execute 'querySelectorAll'")) { - // keep jquery for backwards compatibility until all tests are converted - res = [...$(target).find(selector)]; - } else { - throw error; - } - } - const filteredRes = res.filter( - (el) => - (text === undefined || el.textContent.trim() === text) && - (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})`; - if (click) { - 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_seen_indicator_tests.js b/addons/mail/static/tests/message/message_seen_indicator_tests.js index f2430dfc101..ac54690c66d 100644 --- a/addons/mail/static/tests/message/message_seen_indicator_tests.js +++ b/addons/mail/static/tests/message/message_seen_indicator_tests.js @@ -1,7 +1,7 @@ /* @odoo-module */ import { Command } from "@mail/../tests/helpers/command"; -import { contains, startServer, start, afterNextRender } from "@mail/../tests/helpers/test_utils"; +import { contains, startServer, start } from "@mail/../tests/helpers/test_utils"; QUnit.module("message_seen_indicator"); @@ -197,12 +197,10 @@ QUnit.test("'channel_fetch' notification received is correctly handled", async ( const channel = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]])[0]; // Simulate received channel fetched notification - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", { - channel_id: channelId, - last_message_id: 100, - partner_id: partnerId, - }); + pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", { + channel_id: channelId, + last_message_id: 100, + partner_id: partnerId, }); await contains(".o-mail-MessageSeenIndicator i"); }); @@ -231,12 +229,10 @@ QUnit.test("'channel_seen' notification received is correctly handled", async () const channel = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]])[0]; // Simulate received channel seen notification - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/seen", { - channel_id: channelId, - last_message_id: 100, - partner_id: partnerId, - }); + pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/seen", { + channel_id: channelId, + last_message_id: 100, + partner_id: partnerId, }); await contains(".o-mail-MessageSeenIndicator i", { count: 2 }); }); @@ -267,22 +263,18 @@ QUnit.test( const channel = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]])[0]; // Simulate received channel fetched notification - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", { - channel_id: channelId, - last_message_id: 100, - partner_id: partnerId, - }); + pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", { + channel_id: channelId, + last_message_id: 100, + partner_id: partnerId, }); await contains(".o-mail-MessageSeenIndicator i"); // Simulate received channel seen notification - await afterNextRender(() => { - pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/seen", { - channel_id: channelId, - last_message_id: 100, - partner_id: partnerId, - }); + pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/seen", { + channel_id: channelId, + last_message_id: 100, + partner_id: partnerId, }); await contains(".o-mail-MessageSeenIndicator i", { count: 2 }); } diff --git a/addons/mail/static/tests/message/message_tests.js b/addons/mail/static/tests/message/message_tests.js index 085746ed678..250f08e026c 100644 --- a/addons/mail/static/tests/message/message_tests.js +++ b/addons/mail/static/tests/message/message_tests.js @@ -416,9 +416,10 @@ QUnit.test( openDiscuss(channelId); await click(".o-mail-Message [title='Expand']:eq(0)"); await click(".o-mail-Message [title='Edit']"); - const input = (await contains(".o-mail-Message .o-mail-Composer-input"))[0]; - insertText(input, "Goodbye World", { replace: true }); - triggerHotkey("Enter", false); + await insertText(".o-mail-Message .o-mail-Composer-input", "Goodbye World", { + replace: true, + }); + triggerHotkey("Enter"); await contains(".o-mail-MessageInReply-message", { text: "Goodbye World" }); } ); diff --git a/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js b/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js index 5703cdafe11..df809a60958 100644 --- a/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js +++ b/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js @@ -3,13 +3,7 @@ import { Command } from "@mail/../tests/helpers/command"; import { patchBrowserNotification } from "@mail/../tests/helpers/patch_notifications"; import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; -import { - afterNextRender, - click, - contains, - start, - startServer, -} from "@mail/../tests/helpers/test_utils"; +import { click, contains, start, startServer } from "@mail/../tests/helpers/test_utils"; import { browser } from "@web/core/browser/browser"; import { patchWithCleanup, triggerEvent } from "@web/../tests/helpers/utils"; @@ -508,20 +502,18 @@ QUnit.test("Counter is updated when receiving new message", async () => { }); const { env, openDiscuss } = await start(); await openDiscuss(); - await afterNextRender(() => - pyEnv.withUser(userId, () => - env.services.rpc("/mail/message/post", { - thread_id: channelId, - thread_model: "discuss.channel", - post_data: { - body: "Hello world", - message_type: "comment", - }, - context: { - partnerId, - }, - }) - ) + pyEnv.withUser(userId, () => + env.services.rpc("/mail/message/post", { + thread_id: channelId, + thread_model: "discuss.channel", + post_data: { + body: "Hello world", + message_type: "comment", + }, + context: { + partnerId, + }, + }) ); await contains(".o-mail-MessagingMenu-counter", { text: "1" }); }); @@ -911,13 +903,11 @@ QUnit.test("preview for channel should show latest non-deleted message", async ( await click(".o_menu_systray i[aria-label='Messages']"); await contains(".o-mail-NotificationItem-text", { text: "Partner1: message-2" }); // Simulate deletion of message-2 - await afterNextRender(() => - env.services.rpc("/mail/message/update_content", { - message_id: messageId_2, - body: "", - attachment_ids: [], - }) - ); + env.services.rpc("/mail/message/update_content", { + message_id: messageId_2, + body: "", + attachment_ids: [], + }); await contains(".o-mail-NotificationItem-text", { text: "Partner1: message-1" }); }); diff --git a/addons/mail/static/tests/thread/thread_tests.js b/addons/mail/static/tests/thread/thread_tests.js index 2c74fe4e447..1347eb19044 100644 --- a/addons/mail/static/tests/thread/thread_tests.js +++ b/addons/mail/static/tests/thread/thread_tests.js @@ -2,7 +2,6 @@ import { Command } from "@mail/../tests/helpers/command"; import { - afterNextRender, click, contains, dragenterFiles, @@ -296,7 +295,6 @@ QUnit.test( Command.create({ partner_id: partnerId }), ], }); - const deferred = makeDeferred(); const { env, openDiscuss } = await start({ async mockRPC(route, args) { if (args.method === "channel_fetched" && args.args[0] === channelId) { @@ -306,7 +304,6 @@ QUnit.test( } else if (route === "/discuss/channel/set_last_seen_message") { assert.strictEqual(args.channel_id, channelId); assert.step("rpc:set_last_seen_message"); - await deferred; } }, }); @@ -320,7 +317,7 @@ QUnit.test( thread_model: "discuss.channel", }) ); - await afterNextRender(() => deferred.resolve()); + await contains(".o-mail-Message"); assert.verifySteps(["rpc:set_last_seen_message"]); } ); @@ -515,14 +512,12 @@ QUnit.test("new messages separator on receiving new message [REQUIRE FOCUS]", as $(".o-mail-Composer-input")[0].blur(); // simulate receiving a message - await afterNextRender(() => - 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", { count: 2 }); await contains(".o-mail-Thread-newMessage hr + span", { text: "New messages" }); @@ -997,7 +992,7 @@ QUnit.test("can be marked as read while loading", async function () { openDiscuss(undefined); await contains(".o-discuss-badge", { text: "1" }); await click(".o-mail-DiscussSidebarChannel span", { text: "Demo" }); - await afterNextRender(loadDeferred.resolve); + loadDeferred.resolve(); await contains(".o-discuss-badge", { count: 0 }); }); diff --git a/addons/mail/static/tests/web/chatter_tests.js b/addons/mail/static/tests/web/chatter_tests.js index 94e4c81124a..75aaf8022e9 100644 --- a/addons/mail/static/tests/web/chatter_tests.js +++ b/addons/mail/static/tests/web/chatter_tests.js @@ -2,7 +2,6 @@ import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; import { - afterNextRender, click, contains, dragenterFiles, @@ -194,7 +193,7 @@ QUnit.test("Composer type is kept when switching from aside to bottom", async (a assert.doesNotHaveClass($("button:contains(Send message)"), "btn-primary"); }); -QUnit.test("chatter: drop attachments", async (assert) => { +QUnit.test("chatter: drop attachments", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({}); const { openView } = await start(); @@ -215,14 +214,12 @@ QUnit.test("chatter: drop attachments", async (assert) => { name: "text2.txt", }), ]; - await afterNextRender(() => dragenterFiles($(".o-mail-Chatter")[0])); + await dragenterFiles($(".o-mail-Chatter")[0]); await contains(".o-mail-Dropzone"); await contains(".o-mail-AttachmentCard", { count: 0 }); - - await afterNextRender(() => dropFiles($(".o-mail-Dropzone")[0], files)); + await dropFiles($(".o-mail-Dropzone")[0], files); await contains(".o-mail-AttachmentCard", { count: 2 }); - - await afterNextRender(() => dragenterFiles($(".o-mail-Chatter")[0])); + await dragenterFiles($(".o-mail-Chatter")[0]); files = [ await createFile({ content: "hello, world", @@ -230,7 +227,7 @@ QUnit.test("chatter: drop attachments", async (assert) => { name: "text3.txt", }), ]; - await afterNextRender(() => dropFiles($(".o-mail-Dropzone")[0], files)); + await dropFiles($(".o-mail-Dropzone")[0], files); await contains(".o-mail-AttachmentCard", { count: 3 }); });