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':
- `
`,
};
- 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 });
});