From 2f9c4979fa25e6c03a43e1c109fbbee491704285 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 2 Oct 2023 11:20:55 +0200 Subject: [PATCH] [IMP] web,survey: better error management in tests The initial motivation of this commit was to ensure that the qunit test suite doesn't stop when an error is thrown in a test, which could happen if the error was thrown "sufficiently close to the end of the test". Indeed, the "unhandledrejection" event being async, it was sometimes triggered after the end of the test, when the service registry was already reset, and the check of the presence of the error service was wrong, so the error event wasn't default prevented (e.g. await makeView(...) and the view crashes at render time). This led us to rework in more depth the way we deal with errors in tests. Here are a few behaviors we want (probably not exhaustive): - an error in a test must never end the suite (executed in py) - an error in a test must always make the test fail, except if the error is expected in the scenario, which one must be able to state - a test must always wait for potential unhandledrejection events to be triggered before ending. - ideally, we don't want to have to deal with unhandledrejection in each test throwing an error (in order to prevent the suite to stop) To achieve this, we come with the following solution. We introduce a new assertion method, "expectToThrow" which allows to state that during the test, we expect errors to be thrown. It takes a list of error messages that will be compared at the end of the test with the errors that have been thrown during the test. If they differ, a qunit failure is pushed and the test fails. If an error occurs in a test and "expectToThrow" hasn't been called, qunit is directly informed of the error and a failing assertion is done, make the test fail as well. If the error service isn't available in the test environment, we apply the logic above when an "error" or and "unhandledrejection" event is thrown. If the error service is available, we wrap the default handler (typically the one that handles everything that hasn't been handled by specific handlers, like tracebacks) and if we get to it, we apply the logic above. This means that one must call "expectToThrow" if - the error service isn't deployed, or - the thrown error is handled by the default handler, because it is something like a traceback (errors like UserError, ValidationError are graciously handled by the RPCErrorHandler) and thus never reach the default handler. In all cases, we prevent default the event such that the error doesn't make the python test end. Finally, to ensure that "undhandledrejection" events are handled before the test ends, we wait, in the qunit lib, for a setTimeout before ending the test, which ensures that all such events have been dispatched. closes odoo/odoo#137120 Related: odoo/enterprise#48211 Signed-off-by: Samuel Degueldre (sad) --- .../question_page_one2many_field_tests.js | 6 - addons/web/static/lib/qunit/qunit-2.9.1.js | 10 +- .../static/src/core/errors/error_handlers.js | 2 +- .../static/src/core/errors/error_service.js | 26 ++- .../static/tests/core/code_editor_tests.js | 21 +-- .../static/tests/core/dialog_service_tests.js | 18 +-- .../tests/core/errors/error_service_tests.js | 56 ++----- .../core/main_components_container_tests.js | 29 ++-- addons/web/static/tests/qunit.js | 123 +++++++++++---- .../tests/views/fields/binary_field_tests.js | 6 +- .../views/fields/many2one_field_tests.js | 9 -- .../views/fields/one2many_field_tests.js | 23 --- .../tests/views/form/form_view_tests.js | 149 +++--------------- .../web/static/tests/views/list_view_tests.js | 8 +- .../webclient/actions/close_action_tests.js | 21 +-- .../webclient/actions/error_handling_tests.js | 20 +-- .../webclient/actions/load_state_tests.js | 14 +- .../tests/webclient/actions/target_tests.js | 16 +- .../webclient/actions/window_action_tests.js | 90 ----------- 19 files changed, 186 insertions(+), 461 deletions(-) diff --git a/addons/survey/static/tests/components/question_page_one2many_field_tests.js b/addons/survey/static/tests/components/question_page_one2many_field_tests.js index b78c6182727..7f45cff5bad 100644 --- a/addons/survey/static/tests/components/question_page_one2many_field_tests.js +++ b/addons/survey/static/tests/components/question_page_one2many_field_tests.js @@ -2,7 +2,6 @@ import { makeServerError } from "@web/../tests/helpers/mock_server"; import { click, editInput, getFixture, nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; -import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { errorService } from "@web/core/errors/error_service"; import { registry } from "@web/core/registry"; @@ -174,11 +173,6 @@ QUnit.module("QuestionPageOneToManyField", (hooks) => { QUnit.test("A validation error from saving parent form notifies and prevents dialog from closing", async (assert) => { registry.category("services").add("error", errorService); - // need to preventDefault to remove error from console (so python test pass) - const handler = (ev) => ev.preventDefault(); - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - await makeView({ type: "form", resModel: "survey", diff --git a/addons/web/static/lib/qunit/qunit-2.9.1.js b/addons/web/static/lib/qunit/qunit-2.9.1.js index fc886ffaa37..28760e75902 100644 --- a/addons/web/static/lib/qunit/qunit-2.9.1.js +++ b/addons/web/static/lib/qunit/qunit-2.9.1.js @@ -3128,7 +3128,7 @@ }, - finish: function finish() { + finish: async function finish() { config.current = this; // Release the test callback to ensure that anything referenced has been @@ -3148,7 +3148,13 @@ this.pushFailure("Expected at least one assertion, but none were run - call " + "expect(0) to accept zero assertions.", this.stack); } - emit("OdooAfterTestHook", this); // Odoo customization + // Odoo customization + // wait for the task queue to be fully consummed, s.t. if there has been rejected promises + // during the test, the unhandledrejection handlers have been called before the cleanups + // have been executed, for the errors to be properly preventDefaulted if necessary (see + // qunit.js, Error management section). + await new Promise((r) => setTimeout(r, 0)); + emit("OdooAfterTestHook", this); var i, module = this.module, diff --git a/addons/web/static/src/core/errors/error_handlers.js b/addons/web/static/src/core/errors/error_handlers.js index ba31c871e9b..292b357c6af 100644 --- a/addons/web/static/src/core/errors/error_handlers.js +++ b/addons/web/static/src/core/errors/error_handlers.js @@ -146,7 +146,7 @@ const defaultDialogs = new Map([ * @param {UncaughError} error * @returns {boolean} */ -function defaultHandler(env, error) { +export function defaultHandler(env, error) { const DialogComponent = defaultDialogs.get(error.constructor) || ErrorDialog; env.services.dialog.add(DialogComponent, { traceback: error.traceback, diff --git a/addons/web/static/src/core/errors/error_service.js b/addons/web/static/src/core/errors/error_service.js index 498731efb5f..3e147222594 100644 --- a/addons/web/static/src/core/errors/error_service.js +++ b/addons/web/static/src/core/errors/error_service.js @@ -48,21 +48,17 @@ export const errorService = { while (originalError instanceof Error && "cause" in originalError) { originalError = originalError.cause; } - const services = env.services; - if (!services.dialog || !services.notification || !services.rpc) { - // here, the environment is not ready to provide feedback to the user. - // We simply wait 1 sec and try again, just in case the application can - // recover. - if (retry) { - browser.setTimeout(() => { - handleError(uncaughtError, false); - }, 1000); - } - return; - } - for (const handler of registry.category("error_handlers").getAll()) { - if (handler(env, uncaughtError, originalError)) { - break; + for (const [name, handler] of registry.category("error_handlers").getEntries()) { + try { + if (handler(env, uncaughtError, originalError)) { + break; + } + } catch (e) { + console.error( + `A crash occured in error handler ${name} while handling ${uncaughtError}:`, + e + ); + return; } } if (uncaughtError.event && !uncaughtError.event.defaultPrevented) { diff --git a/addons/web/static/tests/core/code_editor_tests.js b/addons/web/static/tests/core/code_editor_tests.js index 5cb00ce0422..f11aa035b19 100644 --- a/addons/web/static/tests/core/code_editor_tests.js +++ b/addons/web/static/tests/core/code_editor_tests.js @@ -13,8 +13,6 @@ import { import { Component, markup, useState, xml } from "@odoo/owl"; import { CodeEditor } from "@web/core/code_editor/code_editor"; import { registerCleanup } from "@web/../tests/helpers/cleanup"; -import { registry } from "@web/core/registry"; -import { errorService } from "@web/core/errors/error_service"; QUnit.module("Web Components", (hooks) => { QUnit.module("Code Editor"); @@ -84,6 +82,8 @@ QUnit.module("Web Components", (hooks) => { }); QUnit.test("CodeEditor shouldn't accepts markup values", async (assert) => { + assert.expectErrors(); + const _console = window.console; window.console = Object.assign(Object.create(_console), { warn(msg) { @@ -93,18 +93,7 @@ QUnit.module("Web Components", (hooks) => { registerCleanup(() => { window.console = _console; }); - registry.category("services").add("error", errorService); - const handler = (ev) => { - assert.step(ev.reason.message); - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); class Parent extends Component { static components = { CodeEditor }; static template = xml``; @@ -121,10 +110,8 @@ QUnit.module("Web Components", (hooks) => { const textMarkup = markup`
Some Text
`; codeEditor.state.value = textMarkup; await nextTick(); // wait for the errorService to be called - assert.verifySteps([ - "[Owl] Unhandled error. Destroying the root component", - "Invalid props for component 'CodeEditor': 'value' is not valid", - ]); + assert.verifySteps(["[Owl] Unhandled error. Destroying the root component"]); + assert.verifyErrors(["Invalid props for component 'CodeEditor': 'value' is not valid"]); }); QUnit.test("onChange props called when code is edited", async (assert) => { diff --git a/addons/web/static/tests/core/dialog_service_tests.js b/addons/web/static/tests/core/dialog_service_tests.js index 2685df5de74..5aabb185dd2 100644 --- a/addons/web/static/tests/core/dialog_service_tests.js +++ b/addons/web/static/tests/core/dialog_service_tests.js @@ -7,7 +7,6 @@ import { registry } from "@web/core/registry"; import { notificationService } from "@web/core/notifications/notification_service"; import { uiService } from "@web/core/ui/ui_service"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; -import { registerCleanup } from "../helpers/cleanup"; import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env"; import { makeFakeLocalizationService, makeFakeRPCService } from "../helpers/mock_services"; import { @@ -197,7 +196,8 @@ QUnit.test("Interactions between multiple dialogs", async (assert) => { }); QUnit.test("dialog component crashes", async (assert) => { - assert.expect(4); + assert.expect(3); + assert.expectErrors(); class FailingDialog extends Component { setup() { @@ -217,18 +217,6 @@ QUnit.test("dialog component crashes", async (assert) => { }, }); - const handler = (ev) => { - assert.step("error"); - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); - const rpc = makeFakeRPCService(); serviceRegistry.add("rpc", rpc, { force: true }); serviceRegistry.add("notification", notificationService); @@ -239,7 +227,7 @@ QUnit.test("dialog component crashes", async (assert) => { env.services.dialog.add(FailingDialog); await prom; - assert.verifySteps(["error"]); assert.containsOnce(target, ".modal"); assert.containsOnce(target, ".modal .o_error_dialog"); + assert.verifyErrors(["Some Error"]); }); diff --git a/addons/web/static/tests/core/errors/error_service_tests.js b/addons/web/static/tests/core/errors/error_service_tests.js index 4790b7e2782..b9b6c048f01 100644 --- a/addons/web/static/tests/core/errors/error_service_tests.js +++ b/addons/web/static/tests/core/errors/error_service_tests.js @@ -25,6 +25,7 @@ import { import { getFixture, makeDeferred, mount, nextTick, patchWithCleanup } from "../../helpers/utils"; import { Component, xml, onError, OwlError, onWillStart } from "@odoo/owl"; +import { defaultHandler } from "@web/core/errors/error_handlers"; const errorDialogRegistry = registry.category("error_dialogs"); const errorHandlerRegistry = registry.category("error_handlers"); const serviceRegistry = registry.category("services"); @@ -251,6 +252,7 @@ QUnit.test("will let handlers from the registry handle errors first", async (ass assert.strictEqual(originalError, error); assert.strictEqual(env.someValue, 14); assert.step("in handler"); + return true; }); const testEnv = await makeTestEnv(); testEnv.someValue = 14; @@ -271,6 +273,7 @@ QUnit.test("originalError is the root cause of the error chain", async (assert) assert.ok(err.cause instanceof OwlError); // Wrapped by owl assert.strictEqual(err.cause.cause, originalError); // original error assert.step("in handler"); + return true; }); const testEnv = await makeTestEnv(); testEnv.someValue = 14; @@ -318,6 +321,7 @@ QUnit.test("originalError is the root cause of the error chain", async (assert) }); QUnit.test("handle uncaught promise errors", async (assert) => { + assert.expectErrors(); class TestError extends Error {} const error = new TestError(); error.message = "This is an error test"; @@ -340,9 +344,11 @@ QUnit.test("handle uncaught promise errors", async (assert) => { cancelable: true, }); await unhandledRejectionCb(errorEvent); + assert.verifyErrors(["This is an error test"]); }); QUnit.test("handle uncaught client errors", async (assert) => { + assert.expectErrors(); class TestError extends Error {} const error = new TestError(); error.message = "This is an error test"; @@ -364,9 +370,11 @@ QUnit.test("handle uncaught client errors", async (assert) => { cancelable: true, }); await errorCb(errorEvent); + assert.verifyErrors(["This is an error test"]); }); QUnit.test("handle uncaught CORS errors", async (assert) => { + assert.expectErrors(); class TestError extends Error {} const error = new TestError(); error.message = "This is a cors error"; @@ -382,50 +390,14 @@ QUnit.test("handle uncaught CORS errors", async (assert) => { // CORS error event has no colno, no lineno and no filename const errorEvent = new ErrorEvent("error", { error, cancelable: true }); await errorCb(errorEvent); -}); - -QUnit.test("check retry", async (assert) => { - assert.expect(3); - - errorHandlerRegistry.add("__test_handler__", () => { - assert.step("dispatched"); - }); - - const def = makeDeferred(); - patchWithCleanup(browser, { - setTimeout(fn) { - def.then(fn); - }, - }); - - serviceRegistry.remove("dialog"); - await makeTestEnv(); - - class TestError extends Error {} - const error = new TestError(); - error.message = "This is an error test"; - error.name = "TestError"; - - const errorEvent = new PromiseRejectionEvent("error", { - reason: error, - promise: null, - cancelable: true, - }); - await unhandledRejectionCb(errorEvent); - - assert.verifySteps([]); - - serviceRegistry.add("dialog", dialogService); - await nextTick(); - - await def.resolve(); - assert.verifySteps(["dispatched"]); + assert.verifyErrors(["This is a cors error"]); }); QUnit.test("lazy loaded handlers", async (assert) => { + assert.expectErrors(); await makeTestEnv(); const errorEvent = new PromiseRejectionEvent("error", { - reason: new Error(), + reason: new Error("error"), promise: null, cancelable: true, }); @@ -435,10 +407,12 @@ QUnit.test("lazy loaded handlers", async (assert) => { errorHandlerRegistry.add("__test_handler__", () => { assert.step("in handler"); + return true; }); await unhandledRejectionCb(errorEvent); assert.verifySteps(["in handler"]); + assert.verifyErrors(["error"]); // for the first throw, before registering the handler }); // The following test(s) do not want the preventDefault to be done automatically. @@ -450,6 +424,10 @@ QUnit.module("Error Service", { serviceRegistry.add("rpc", makeFakeRPCService()); serviceRegistry.add("localization", makeFakeLocalizationService()); serviceRegistry.add("ui", uiService); + // remove the override of the defaultHandler done in qunit.js + registry + .category("error_handlers") + .add("defaultHandler", defaultHandler, { sequence: 100, force: true }); const windowAddEventListener = browser.addEventListener; browser.addEventListener = (type, cb) => { if (type === "unhandledrejection") { diff --git a/addons/web/static/tests/core/main_components_container_tests.js b/addons/web/static/tests/core/main_components_container_tests.js index 708fa999605..3b26fbf5708 100644 --- a/addons/web/static/tests/core/main_components_container_tests.js +++ b/addons/web/static/tests/core/main_components_container_tests.js @@ -2,10 +2,10 @@ import { MainComponentsContainer } from "@web/core/main_components_container"; import { registry } from "@web/core/registry"; import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env"; -import { patch } from "@web/core/utils/patch"; import { getFixture, mount, nextTick } from "../helpers/utils"; import { Component, useState, xml } from "@odoo/owl"; +import { registerCleanup } from "../helpers/cleanup"; const mainComponentsRegistry = registry.category("main_components"); let target; @@ -38,6 +38,7 @@ QUnit.module("Components", (hooks) => { }); QUnit.test("unmounts erroring main component", async function (assert) { + assert.expectErrors(); const env = await makeTestEnv(); let compA; @@ -67,24 +68,18 @@ QUnit.module("Components", (hooks) => { const handler = (ev) => { assert.step(ev.reason.message); assert.step(ev.reason.cause.message); - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); }; window.addEventListener("unhandledrejection", handler); - // fake error service so that the odoo qunit handlers don't think that they need to handle the error - registry.category("services").add("error", { start: () => {} }); - const unpatch = patch(QUnit, { - onUnhandledRejection: () => {}, + registerCleanup(() => { + window.removeEventListener("unhandledrejection", handler); }); compA.state.shouldThrow = true; await nextTick(); - window.removeEventListener("unhandledrejection", handler); - // unpatch QUnit asap so any other errors can be caught by it - unpatch(); assert.verifySteps([ 'An error occured in the owl lifecycle (see this Error\'s "cause" property)', "BOOM", ]); + assert.verifyErrors(["BOOM"]); assert.equal( target.querySelector(".o-main-components-container").innerHTML, @@ -93,6 +88,8 @@ QUnit.module("Components", (hooks) => { }); QUnit.test("unmounts erroring main component: variation", async function (assert) { + assert.expectErrors(); + const env = await makeTestEnv(); class MainComponentA extends Component {} @@ -122,24 +119,18 @@ QUnit.module("Components", (hooks) => { const handler = (ev) => { assert.step(ev.reason.message); assert.step(ev.reason.cause.message); - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); }; window.addEventListener("unhandledrejection", handler); - // fake error service so that the odoo qunit handlers don't think that they need to handle the error - registry.category("services").add("error", { start: () => {} }); - const unpatch = patch(QUnit, { - onUnhandledRejection: () => {}, + registerCleanup(() => { + window.removeEventListener("unhandledrejection", handler); }); compB.state.shouldThrow = true; await nextTick(); - window.removeEventListener("unhandledrejection", handler); - // unpatch QUnit asap so any other errors can be caught by it - unpatch(); assert.verifySteps([ 'An error occured in the owl lifecycle (see this Error\'s "cause" property)', "BOOM", ]); + assert.verifyErrors(["BOOM"]); assert.equal( target.querySelector(".o-main-components-container").innerHTML, "MainComponentA" diff --git a/addons/web/static/tests/qunit.js b/addons/web/static/tests/qunit.js index bacf1acd6ba..0c6a70c2ccf 100644 --- a/addons/web/static/tests/qunit.js +++ b/addons/web/static/tests/qunit.js @@ -1,15 +1,12 @@ /** @odoo-module */ import { isVisible as isElemVisible } from "@web/core/utils/ui"; -import { UncaughtClientError, UncaughtPromiseError } from "@web/core/errors/error_service"; -import { - completeUncaughtError, - fullTraceback, - fullAnnotatedTraceback, -} from "@web/core/errors/error_utils"; +import { fullTraceback, fullAnnotatedTraceback } from "@web/core/errors/error_utils"; import { registry } from "@web/core/registry"; import { escape } from "@web/core/utils/strings"; +const consoleError = console.error; + function setQUnitDebugMode() { owl.whenReady(() => document.body.classList.add("debug")); // make the test visible to the naked eye QUnit.config.debug = true; // allows for helper functions to behave differently (logging, the HTML element in which the test occurs etc...) @@ -207,6 +204,19 @@ export function setupQUnit() { function isNotVisible(el, msg) { return _checkVisible(el, false, msg); } + function expectErrors() { + QUnit.config.current.expectErrors = true; + QUnit.config.current.unverifiedErrors = []; + } + function verifyErrors(expectedErrors) { + if (!QUnit.config.current.expectErrors) { + QUnit.pushFailure(`assert.expectErrors() must be called at the beginning of the test`); + return; + } + const unverifiedErrors = QUnit.config.current.unverifiedErrors; + QUnit.config.current.assert.deepEqual(unverifiedErrors, expectedErrors, "verifying errors"); + QUnit.config.current.unverifiedErrors = []; + } QUnit.assert.containsN = containsN; QUnit.assert.containsNone = containsNone; QUnit.assert.containsOnce = containsOnce; @@ -215,6 +225,8 @@ export function setupQUnit() { QUnit.assert.hasAttrValue = hasAttrValue; QUnit.assert.isVisible = isVisible; QUnit.assert.isNotVisible = isNotVisible; + QUnit.assert.expectErrors = expectErrors; + QUnit.assert.verifyErrors = verifyErrors; // ----------------------------------------------------------------------------- // QUnit logs @@ -229,7 +241,7 @@ export function setupQUnit() { const messages = errorMessages.slice(); errorMessages = []; const infos = await Promise.all(messages); - console.error(infos.map((info) => info.error || info).join("\n")); + consoleError(infos.map((info) => info.error || info).join("\n")); // Only log the source of the errors in "info" log level to allow matching the same // error with its log message, as source contains asset file name which changes console.info( @@ -609,12 +621,54 @@ export function setupQUnit() { document.head.appendChild(el); }); - const { onUnhandledRejection, onError } = QUnit; + // ----------------------------------------------------------------------------- + // Error management + // ----------------------------------------------------------------------------- + + QUnit.on("OdooAfterTestHook", (info) => { + const { expectErrors, unverifiedErrors } = QUnit.config.current; + if (expectErrors && unverifiedErrors.length) { + QUnit.pushFailure( + `Expected assert.verifyErrors() to be called before end of test. Unverified errors: ${unverifiedErrors}` + ); + } + }); + + const { onUnhandledRejection } = QUnit; QUnit.onUnhandledRejection = () => {}; QUnit.onError = () => {}; + + console.error = function () { + if (QUnit.config.current) { + QUnit.pushFailure(`console.error called with "${arguments[0]}"`); + } else { + consoleError(...arguments); + } + }; + + function onUncaughtErrorInTest(error) { + if (!QUnit.config.current.expectErrors) { + // we did not expect any error, so notify qunit to add a failure + onUnhandledRejection(error); + } else { + // we expected errors, so store it, it will be checked later (see verifyErrors) + while (error instanceof Error && "cause" in error) { + error = error.cause; + } + QUnit.config.current.unverifiedErrors.push(error.message); + } + } + + // e.g. setTimeout(() => throw new Error()) (event handler crashes synchronously) window.addEventListener("error", async (ev) => { - // don't do anything if error service is up and we are in a test - if (registry.category("services").get("error", false) && QUnit.config.current) { + if (!QUnit.config.current) { + return; // we are not in a test -> do nothing + } + // do not log to the console as this will kill python test early + ev.preventDefault(); + // if the error service is deployed, we'll get to the patched default handler below if no + // other handler handled the error, so do nothing here + if (registry.category("services").get("error", false)) { return; } if ( @@ -623,33 +677,36 @@ export function setupQUnit() { ) { return; } - // Do not log to the console as this will kill python test early - ev.preventDefault(); - const { error: originalError } = ev; - const uncaughtError = new UncaughtClientError(); - if (originalError instanceof Error) { - originalError.errorEvent = ev; - await completeUncaughtError(uncaughtError, originalError); - originalError.stacktrace = uncaughtError.traceback; - } - onError(originalError); + onUncaughtErrorInTest(ev.error); }); + // e.g. Promise.resolve().then(() => throw new Error()) (crash in event handler after async boundary) window.addEventListener("unhandledrejection", async (ev) => { - // don't do anything if error service is up and we are in a test - if (registry.category("services").get("error", false) && QUnit.config.current) { + if (!QUnit.config.current) { + return; // we are not in a test -> do nothing + } + // do not log to the console as this will kill python test early + ev.preventDefault(); + // if the error service is deployed, we'll get to the patched default handler below if no + // other handler handled the error, so do nothing here + if (registry.category("services").get("error", false)) { return; } - // Do not log to the console as this will kill python test early - ev.preventDefault(); - const originalError = ev.reason; - const uncaughtError = new UncaughtPromiseError(); - uncaughtError.unhandledRejectionEvent = ev; - if (originalError instanceof Error) { - originalError.errorEvent = ev; - await completeUncaughtError(uncaughtError, originalError); - originalError.stack = uncaughtError.traceback; - } - onUnhandledRejection(originalError); + onUncaughtErrorInTest(ev.reason); + }); + + // This is an approximation, but we can't directly import the default error handler, because + // it's not the same in all tested environments (e.g. /web and /pos), so we get the last item + // from the handler registry and assume it is the default one, which handles all "not already + // handled" errors, like tracebacks. + const errorHandlerRegistry = registry.category("error_handlers"); + const [defaultHandlerName, defaultHandler] = errorHandlerRegistry.getEntries().at(-1); + const testDefaultHandler = (env, uncaughtError, originalError) => { + onUncaughtErrorInTest(originalError); + return defaultHandler(env, uncaughtError, originalError); + }; + errorHandlerRegistry.add(defaultHandlerName, testDefaultHandler, { + sequence: Number.POSITIVE_INFINITY, + force: true, }); } diff --git a/addons/web/static/tests/views/fields/binary_field_tests.js b/addons/web/static/tests/views/fields/binary_field_tests.js index f4196c2e11e..9a7f3996a6b 100644 --- a/addons/web/static/tests/views/fields/binary_field_tests.js +++ b/addons/web/static/tests/views/fields/binary_field_tests.js @@ -506,8 +506,10 @@ QUnit.module("Fields", (hooks) => { QUnit.test("Binary filename doesn't exceed 255 bytes", async function (assert) { const LARGE_BINARY_FILE = BINARY_FILE.repeat(5); - assert.ok((LARGE_BINARY_FILE.length / 4 * 3) > MAX_FILENAME_SIZE_BYTES, - "The initial binary file should be larger than max bytes that can represent the filename"); + assert.ok( + (LARGE_BINARY_FILE.length / 4) * 3 > MAX_FILENAME_SIZE_BYTES, + "The initial binary file should be larger than max bytes that can represent the filename" + ); serverData.models.partner.fields.document.default = LARGE_BINARY_FILE; await makeView({ serverData, diff --git a/addons/web/static/tests/views/fields/many2one_field_tests.js b/addons/web/static/tests/views/fields/many2one_field_tests.js index 088ce052bf2..2c09edbca4e 100644 --- a/addons/web/static/tests/views/fields/many2one_field_tests.js +++ b/addons/web/static/tests/views/fields/many2one_field_tests.js @@ -1,6 +1,5 @@ /** @odoo-module **/ -import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { makeTestEnv } from "@web/../tests/helpers/mock_env"; import { makeServerError } from "@web/../tests/helpers/mock_server"; import { @@ -2950,14 +2949,6 @@ QUnit.module("Fields", (hooks) => { registry.category("services").add("error", errorService); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); - serverData.views = { "product,false,form": '
', }; diff --git a/addons/web/static/tests/views/fields/one2many_field_tests.js b/addons/web/static/tests/views/fields/one2many_field_tests.js index a4863babf62..f04c92f5699 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -8931,29 +8931,6 @@ QUnit.module("Fields", (hooks) => { QUnit.test("display correct value after validation error", async function (assert) { assert.expect(5); - /* - * By-pass QUnit's and test's error handling because the error service needs to be active - */ - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); - - const originOnunhandledrejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originOnunhandledrejection; - }); - /* - * End By pass error handling - */ - serviceRegistry.add("error", errorService); function validationHandler(env, error, originalError) { if (originalError.data.name === "odoo.exceptions.ValidationError") { diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 6731f61a5cd..c58cbf9b34a 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -1,7 +1,6 @@ /** @odoo-module **/ import { Component, EventBus, xml } from "@odoo/owl"; -import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { makeServerError } from "@web/../tests/helpers/mock_server"; import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; import { @@ -3289,7 +3288,8 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("buttons in form view", async function (assert) { - assert.expect(10); + assert.expect(12); + assert.expectErrors(); const mockedActionService = { start() { @@ -3300,7 +3300,7 @@ QUnit.module("Views", (hooks) => { assert.strictEqual(params.resId, 2); params.onClose(); } else { - return Promise.reject(); + throw makeServerError(); } }, }; @@ -3337,6 +3337,7 @@ QUnit.module("Views", (hooks) => { // click on p (will succeed and reload) await click(target.querySelector(".o_form_statusbar button.p")); + assert.verifyErrors([]); // click on s (will fail) await click(target.querySelector(".o_form_statusbar button.s")); @@ -3348,6 +3349,7 @@ QUnit.module("Views", (hooks) => { "web_read", // reload (successfully clicked on p) "some_method", ]); + assert.verifyErrors(["Odoo Server Error"]); }); QUnit.test("buttons classes in form view", async function (assert) { @@ -6322,13 +6324,6 @@ QUnit.module("Views", (hooks) => { QUnit.test("onchange returns an error", async function (assert) { registry.category("services").add("error", errorService); registry.category("error_dialogs").add("odoo.exceptions.UserError", WarningDialog); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); serverData.models.partner.onchanges = { int_field: () => {} }; await makeView({ @@ -9975,19 +9970,6 @@ QUnit.module("Views", (hooks) => { QUnit.test("form view is not broken if save operation fails", async function (assert) { registry.category("services").add("error", errorService); - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); await makeView({ type: "form", @@ -9998,7 +9980,7 @@ QUnit.module("Views", (hooks) => { mockRPC(route, args) { assert.step(args.method); if (args.method === "web_save" && args.args[1].foo === "incorrect value") { - return Promise.reject(new Error("Odoo Server Error")); + throw makeServerError(); } }, }); @@ -10023,19 +10005,6 @@ QUnit.module("Views", (hooks) => { "form view is not broken if save failed in readonly mode on field changed", async function (assert) { registry.category("services").add("error", errorService); - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); let failFlag = false; await makeView({ type: "form", @@ -10053,7 +10022,7 @@ QUnit.module("Views", (hooks) => { if (args.method === "web_save") { assert.step("web_save"); if (failFlag) { - return Promise.reject(new Error("Odoo Server Error")); + throw makeServerError(); } } else if (args.method === "web_read") { assert.step("web_read"); @@ -10890,10 +10859,10 @@ QUnit.module("Views", (hooks) => { }); QUnit.test("keep editing after call_button fail", async function (assert) { - assert.expect(4); + assert.expect(5); + assert.expectErrors(); let values; - const mockedActionService = { start() { return { @@ -10903,7 +10872,7 @@ QUnit.module("Views", (hooks) => { ["post", "object"], "the action should be correctly executed" ); - return Promise.reject(); + throw makeServerError(); }, }; }, @@ -10941,6 +10910,9 @@ QUnit.module("Views", (hooks) => { product_id: false, }; await click(target.querySelector("button.p")); + + assert.verifyErrors(["Odoo Server Error"]); + // edit the new row again and set a many2one value await click( target.querySelectorAll(".o_form_view .o_field_one2many .o_data_row .o_data_cell")[1] @@ -11919,20 +11891,6 @@ QUnit.module("Views", (hooks) => { registry.category("services").add("error", errorService); - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); - serverData.actions[1] = { id: 1, name: "Partner", @@ -12540,20 +12498,6 @@ QUnit.module("Views", (hooks) => { registry.category("services").add("error", errorService); - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); - await makeView({ type: "form", resModel: "partner", @@ -12611,20 +12555,6 @@ QUnit.module("Views", (hooks) => { registry.category("services").add("error", errorService); - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); - await makeView({ type: "form", resModel: "partner", @@ -12650,13 +12580,6 @@ QUnit.module("Views", (hooks) => { QUnit.test("no 'oh snap' error when clicking on a view button", async (assert) => { registry.category("services").add("error", errorService); registry.category("error_dialogs").add("odoo.exceptions.UserError", WarningDialog); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); await makeView({ type: "form", @@ -12691,19 +12614,6 @@ QUnit.module("Views", (hooks) => { registry.category("services").add("error", errorService); registry.category("error_dialogs").add("odoo.exceptions.UserError", WarningDialog); - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - // remove the override in qunit.js that swallows unhandledrejection errors - // s.t. we let the error service handle them - const originalOnUnhandledRejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originalOnUnhandledRejection; - }); serverData.views = { "partner,false,form": `
`, @@ -12891,18 +12801,7 @@ QUnit.module("Views", (hooks) => { ); QUnit.test("Action Button clicked with failing action", async function (assert) { - const handler = (ev) => { - assert.step("error"); - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - // fake error service so that the odoo qunit handlers don't think that they need to handle the error - registry.category("services").add("error", { start: () => {} }); - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); + assert.expectErrors(); class MyComponent extends Component { setup() { @@ -12939,7 +12838,7 @@ QUnit.module("Views", (hooks) => { await click(target.querySelector(".oe_stat_button")); assert.containsOnce(target, ".o_form_view .test"); - assert.verifySteps(["error"]); + assert.verifyErrors(["test"]); }); QUnit.test("form view with edit='0' but create='1', existing record", async (assert) => { @@ -13286,7 +13185,7 @@ QUnit.module("Views", (hooks) => { arch: `
`, mockRPC(route, { method }) { if (method === "web_save") { - return Promise.reject(false); + throw makeServerError(); } }, }); @@ -13596,12 +13495,7 @@ QUnit.module("Views", (hooks) => { QUnit.test( "coming to an action with an error from a form view with a dirty x2m", async function (assert) { - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); + assert.expectErrors(); class TestClientAction extends Component { setup() { @@ -13673,6 +13567,7 @@ QUnit.module("Views", (hooks) => { await click(target, ".test_widget button"); await nextTick(); + assert.verifyErrors(["Something went wrong"]); // Close ErrorDialog await click(target, ".o_dialog .btn-close"); @@ -13689,12 +13584,7 @@ QUnit.module("Views", (hooks) => { QUnit.test( "coming to an action with an error from a form view with a record in creation", async function (assert) { - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); + assert.expectErrors(); class TestClientAction extends Component { setup() { @@ -13762,6 +13652,7 @@ QUnit.module("Views", (hooks) => { await click(target, ".test_widget button"); await nextTick(); + assert.verifyErrors(["Something went wrong"]); // Close ErrorDialog await click(target, ".o_dialog .btn-close"); diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index ffc58ea43e3..489eeaf5da1 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -20,7 +20,6 @@ import { RelationalModel } from "@web/model/relational_model/relational_model"; import { actionService } from "@web/webclient/actions/action_service"; import { getPickerApplyButton, getPickerCell } from "../core/datetime/datetime_test_helpers"; import { makeFakeLocalizationService, makeFakeUserService } from "../helpers/mock_services"; -import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { addRow, click, @@ -65,6 +64,7 @@ import { } from "../search/helpers"; import { createWebClient, doAction, loadState } from "../webclient/helpers"; import { makeView, makeViewInDialog, setupViewRegistries } from "./helpers"; +import { makeServerError } from "../helpers/mock_server"; const fieldRegistry = registry.category("fields"); const serviceRegistry = registry.category("services"); @@ -18361,10 +18361,6 @@ QUnit.module("Views", (hooks) => { "edit a record then select another record with a throw error when saving", async function (assert) { serviceRegistry.add("error", errorService); - // need to preventDefault to remove error from console (so python test pass) - const handler = (ev) => ev.preventDefault(); - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); await makeView({ type: "list", @@ -18376,7 +18372,7 @@ QUnit.module("Views", (hooks) => { `, mockRPC(route, args) { if (args.method === "web_save") { - throw new Error("Can't write"); + throw makeServerError({ message: "Can't write" }); } }, }); diff --git a/addons/web/static/tests/webclient/actions/close_action_tests.js b/addons/web/static/tests/webclient/actions/close_action_tests.js index f6eef8e0e2f..a66cd627a6a 100644 --- a/addons/web/static/tests/webclient/actions/close_action_tests.js +++ b/addons/web/static/tests/webclient/actions/close_action_tests.js @@ -1,11 +1,9 @@ /** @odoo-module **/ import testUtils from "@web/../tests/legacy/helpers/test_utils"; -import { registerCleanup } from "../../helpers/cleanup"; import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils"; import { createWebClient, doAction, getActionManagerServerData } from "./../helpers"; -import { registry } from "@web/core/registry"; import { formView } from "@web/views/form/form_view"; import { listView } from "../../../src/views/list/list_view"; @@ -180,19 +178,8 @@ QUnit.module("ActionManager", (hooks) => { ); QUnit.test("web client is not deadlocked when a view crashes", async function (assert) { - assert.expect(6); - const handler = (ev) => { - assert.step("error"); - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - // fake error service so that the odoo qunit handlers don't think that they need to handle the error - registry.category("services").add("error", { start: () => {} }); - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); + assert.expect(4); + assert.expectErrors(); const readOnFirstRecordDef = testUtils.makeTestPromise(); const mockRPC = (route, { method, args, kwargs }) => { @@ -205,10 +192,10 @@ QUnit.module("ActionManager", (hooks) => { // open first record in form view. this will crash and will not // display a form view await testUtils.dom.click($(target).find(".o_list_view .o_data_cell:first")); - assert.verifySteps([]); readOnFirstRecordDef.reject(new Error("not working as intended")); await nextTick(); - assert.verifySteps(["error"]); + assert.verifyErrors(["not working as intended"]); + assert.containsOnce(target, ".o_list_view", "there should still be a list view in dom"); // open another record, the read will not crash await testUtils.dom.click( diff --git a/addons/web/static/tests/webclient/actions/error_handling_tests.js b/addons/web/static/tests/webclient/actions/error_handling_tests.js index 17750058f01..ddcd05a0e7f 100644 --- a/addons/web/static/tests/webclient/actions/error_handling_tests.js +++ b/addons/web/static/tests/webclient/actions/error_handling_tests.js @@ -2,8 +2,7 @@ import { registry } from "@web/core/registry"; import { createWebClient, doAction, getActionManagerServerData } from "./../helpers"; -import { registerCleanup } from "../../helpers/cleanup"; -import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils"; +import { click, getFixture, nextTick } from "../../helpers/utils"; import { errorService } from "@web/core/errors/error_service"; import { Component, xml } from "@odoo/owl"; @@ -56,23 +55,17 @@ QUnit.module("ActionManager", (hooks) => { }); QUnit.test("error in a client action (after the first rendering)", async function (assert) { - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); - + assert.expectErrors(); registry.category("services").add("error", errorService); class Boom extends Component { setup() { this.boom = false; } + get a() { + // a bit artificial, but makes the test firefox compliant + throw new Error("Cannot read properties of undefined (reading 'b')"); + } onClick() { this.boom = true; this.render(); @@ -93,5 +86,6 @@ QUnit.module("ActionManager", (hooks) => { await nextTick(); assert.containsOnce(target, ".my_button"); assert.containsOnce(target, ".o_error_dialog"); + assert.verifyErrors(["Cannot read properties of undefined (reading 'b')"]); }); }); diff --git a/addons/web/static/tests/webclient/actions/load_state_tests.js b/addons/web/static/tests/webclient/actions/load_state_tests.js index 3ef625a2655..0ddfd7d2ef1 100644 --- a/addons/web/static/tests/webclient/actions/load_state_tests.js +++ b/addons/web/static/tests/webclient/actions/load_state_tests.js @@ -3,7 +3,6 @@ import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; import { WebClient } from "@web/webclient/webclient"; -import { registerCleanup } from "../../helpers/cleanup"; import { makeTestEnv } from "../../helpers/mock_env"; import { click, @@ -741,17 +740,7 @@ QUnit.module("ActionManager", (hooks) => { QUnit.test("initial action crashes", async (assert) => { assert.expect(8); - - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); + assert.expectErrors(); browser.location.hash = "#action=__test__client__action__&menu_id=1"; const ClientAction = registry.category("actions").get("__test__client__action__"); @@ -769,6 +758,7 @@ QUnit.module("ActionManager", (hooks) => { const webClient = await createWebClient({ serverData }); assert.verifySteps(["clientAction setup"]); await nextTick(); + assert.expectErrors(["my error"]); assert.containsOnce(target, ".o_error_dialog"); await click(target, ".modal-header .btn-close"); assert.containsNone(target, ".o_error_dialog"); diff --git a/addons/web/static/tests/webclient/actions/target_tests.js b/addons/web/static/tests/webclient/actions/target_tests.js index 3ead4c79837..d825b4354fe 100644 --- a/addons/web/static/tests/webclient/actions/target_tests.js +++ b/addons/web/static/tests/webclient/actions/target_tests.js @@ -4,7 +4,6 @@ import testUtils from "@web/../tests/legacy/helpers/test_utils"; import { registry } from "@web/core/registry"; import { click, getFixture, patchWithCleanup, makeDeferred, nextTick } from "../../helpers/utils"; import { createWebClient, doAction, getActionManagerServerData } from "./../helpers"; -import { registerCleanup } from "../../helpers/cleanup"; import { errorService } from "@web/core/errors/error_service"; import { useService } from "@web/core/utils/hooks"; import { ClientErrorDialog } from "@web/core/errors/error_dialogs"; @@ -266,18 +265,8 @@ QUnit.module("ActionManager", (hooks) => { }); QUnit.test("do not commit a dialog in error", async (assert) => { - assert.expect(6); - - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); + assert.expect(7); + assert.expectErrors(); class ErrorClientAction extends Component { setup() { @@ -333,6 +322,7 @@ QUnit.module("ActionManager", (hooks) => { assert.ok( target.querySelector(".modal-body .o_error_detail").textContent.includes("my error") ); + assert.verifyErrors(["my error"]); await click(target, ".modal-footer .btn-primary"); assert.containsNone(target, ".modal"); diff --git a/addons/web/static/tests/webclient/actions/window_action_tests.js b/addons/web/static/tests/webclient/actions/window_action_tests.js index cd665a9ab71..ab4f7cde175 100644 --- a/addons/web/static/tests/webclient/actions/window_action_tests.js +++ b/addons/web/static/tests/webclient/actions/window_action_tests.js @@ -13,7 +13,6 @@ import { useSetupAction } from "@web/webclient/actions/action_hook"; import { clearUncommittedChanges } from "@web/webclient/actions/action_service"; import testUtils from "@web/../tests/legacy/helpers/test_utils"; import { errorService } from "../../../src/core/errors/error_service"; -import { registerCleanup } from "../../helpers/cleanup"; import { click, clickSave, @@ -521,29 +520,6 @@ QUnit.module("ActionManager", (hooks) => { QUnit.test("A new form view can be reloaded after a failed one", async function (assert) { assert.expect(5); - /* - * By-pass QUnit's and test's error handling because the error service needs to be active - */ - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); - - const originOnunhandledrejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originOnunhandledrejection; - }); - /* - * End By pass error handling - */ - const webClient = await createWebClient({ serverData }); serviceRegistry.add("error", errorService); @@ -2127,29 +2103,6 @@ QUnit.module("ActionManager", (hooks) => { }); QUnit.test("window action in target new fails (onchange)", async (assert) => { - /* - * By-pass QUnit's and test's error handling because the error service needs to be active - */ - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); - - const originOnunhandledrejection = window.onunhandledrejection; - window.onunhandledrejection = () => {}; - registerCleanup(() => { - window.onunhandledrejection = originOnunhandledrejection; - }); - /* - * End By pass error handling - */ - const warningOpened = makeDeferred(); class WarningDialogWait extends WarningDialog { setup() { @@ -2362,26 +2315,6 @@ QUnit.module("ActionManager", (hooks) => { QUnit.test("click on breadcrumb of a deleted record", async function (assert) { serviceRegistry.add("error", errorService); - // In tests we catch unhandledrejection of promises rejected with something that isn't an - // error (see tests/qunit.js). In this scenario, with the legacy basic_model, the promise - // is rejected with undefined, so without the following lines, we can't reproduce the issue, - // which was that the error handlers were executed in a wrong order, and one of them crashed. - const windowUnhandledReject = window.onunhandledrejection; - window.onunhandledrejection = null; - registerCleanup(() => { - window.onunhandledrejection = windowUnhandledReject; - }); - - const handler = (ev) => { - // need to preventDefault to remove error from console (so python test pass) - ev.preventDefault(); - }; - window.addEventListener("unhandledrejection", handler); - registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); - patchWithCleanup(QUnit, { - onUnhandledRejection: () => {}, - }); - serverData.views["partner,false,form"] = `