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"] = `