diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index c40bee95e26..6a3dd3dac79 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -449,7 +449,6 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/js/services/config.js', 'web/static/src/legacy/js/services/core.js', 'web/static/src/legacy/js/services/local_storage_service.js', - 'web/static/src/legacy/js/core/error_utils.js', 'web/static/src/legacy/js/services/session_storage_service.js', 'web/static/src/legacy/js/common_env.js', 'web/static/src/legacy/js/widgets/name_and_signature.js', 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 f456cc15750..a78a25ecbbc 100644 --- a/addons/web/static/lib/qunit/qunit-2.9.1.js +++ b/addons/web/static/lib/qunit/qunit-2.9.1.js @@ -5340,12 +5340,12 @@ assertList.appendChild(assertLi); // Odoo Customisation!!! - // Crappy hack to display traceback with sourcemaps if debug=assets - if (lastError && QUnit.annotateTraceback && debug && debug.includes("assets")) { + // Crappy hack to display full tracebacks (with sourcemaps if debug=assets) + if (lastError && QUnit.annotateTraceback) { const pre = assertLi.querySelector("pre"); - - QUnit.annotateTraceback(lastError).then(traceback => { + details.annotateProm = QUnit.annotateTraceback(lastError).then(traceback => { pre.innerText = traceback; + details.source = traceback; }); lastError = null; } diff --git a/addons/web/static/src/core/errors/error_service.js b/addons/web/static/src/core/errors/error_service.js index 89e8969af12..563ab3ad8e7 100644 --- a/addons/web/static/src/core/errors/error_service.js +++ b/addons/web/static/src/core/errors/error_service.js @@ -3,7 +3,7 @@ import { browser } from "../browser/browser"; import { _lt } from "../l10n/translation"; import { registry } from "../registry"; -import { annotateTraceback, formatTraceback, getErrorTechnicalName } from "./error_utils"; +import { completeUncaughtError, getErrorTechnicalName } from "./error_utils"; /** * Uncaught Errors have 4 properties: @@ -40,39 +40,6 @@ export class UncaughtCorsError extends UncaughtError { } } -/** - * @param {UncaughtError} uncaughtError - * @param {Error} originalError - * @returns {string} - */ -function combineErrorNames(uncaughtError, originalError) { - const originalErrorName = getErrorTechnicalName(originalError); - const uncaughtErrorName = getErrorTechnicalName(uncaughtError); - if (originalErrorName === Error.name) { - return uncaughtErrorName; - } else { - return `${uncaughtErrorName} > ${originalErrorName}`; - } -} - -/** - * @param {import("../../env").OdooEnv} env - * @param {UncaughtError} uncaughtError - * @param {Error} originalError - * @returns {Promise} - */ -async function completeUncaughtError(env, uncaughtError, originalError) { - uncaughtError.name = combineErrorNames(uncaughtError, originalError); - if (env.debug && env.debug.includes("assets")) { - uncaughtError.traceback = await annotateTraceback(originalError); - } else { - uncaughtError.traceback = formatTraceback(originalError); - } - if (originalError.message) { - uncaughtError.message = `${uncaughtError.message} > ${originalError.message}`; - } -} - export const errorService = { start(env) { function handleError(error, originalError, retry = true) { @@ -93,6 +60,15 @@ export const errorService = { break; } } + if ( + originalError instanceof Error && + originalError.errorEvent && + !originalError.errorEvent.defaultPrevented + ) { + // Log the full traceback instead of letting the browser log the incomplete one + originalError.errorEvent.preventDefault(); + console.error(error.traceback); + } } browser.addEventListener("error", async (ev) => { @@ -117,27 +93,23 @@ export const errorService = { ); } else { uncaughtError = new UncaughtClientError(); - await completeUncaughtError(env, uncaughtError, originalError); + if (originalError instanceof Error) { + originalError.errorEvent = ev; + const annotated = env.debug && env.debug.includes("assets"); + await completeUncaughtError(uncaughtError, originalError, annotated); + } } handleError(uncaughtError, originalError); }); browser.addEventListener("unhandledrejection", async (ev) => { - let originalError = ev.reason; - // note: unwrapping error like this means that the error handlers will - // only be aware of the cause, not of the current error (which may - // show more interesting information, such as the owl lifecycle - // method that caused the error. But doing it in the best way (traceback - // from error, and handled by correct error handler requires a small - // scale refactoring of the error handling code. - originalError = - originalError instanceof Error && "cause" in originalError - ? originalError.cause - : originalError; + const originalError = ev.reason; const uncaughtError = new UncaughtPromiseError(); uncaughtError.unhandledRejectionEvent = ev; if (originalError instanceof Error) { - await completeUncaughtError(env, uncaughtError, originalError); + originalError.errorEvent = ev; + const annotated = env.debug && env.debug.includes("assets"); + await completeUncaughtError(uncaughtError, originalError, annotated); } handleError(uncaughtError, originalError); }); diff --git a/addons/web/static/src/core/errors/error_utils.js b/addons/web/static/src/core/errors/error_utils.js index 56364b5610e..d63d722562a 100644 --- a/addons/web/static/src/core/errors/error_utils.js +++ b/addons/web/static/src/core/errors/error_utils.js @@ -3,6 +3,102 @@ import { loadJS } from "../assets"; import { isBrowserChrome } from "../browser/feature_detection"; +/** @typedef {import("./error_service").UncaughtError} UncaughtError */ + +/** + * @param {UncaughtError} uncaughtError + * @param {Error} originalError + * @returns {string} + */ +function combineErrorNames(uncaughtError, originalError) { + const originalErrorName = getErrorTechnicalName(originalError); + const uncaughtErrorName = getErrorTechnicalName(uncaughtError); + if (originalErrorName === Error.name) { + return uncaughtErrorName; + } else { + return `${uncaughtErrorName} > ${originalErrorName}`; + } +} + +/** + * Returns the full traceback for an error chain based on error causes + * + * @param {Error} error + * @returns {string} + */ +export function fullTraceback(error) { + let traceback = formatTraceback(error); + let current = error.cause; + while (current) { + traceback += `\n\nCaused by: ${ + current instanceof Error ? formatTraceback(current) : current + }`; + current = current.cause; + } + return traceback; +} + +/** + * Returns the full annotated traceback for an error chain based on error causes + * + * @param {Error} error + * @returns {Promise} + */ +export async function fullAnnotatedTraceback(error) { + if (error.annotatedTraceback) { + return error.annotatedTraceback; + } + // If we don't call preventDefault synchronously while handling the error + // event, the error will be logged in the console with an unannotated + // traceback. This is a problem because annotating a traceback cannot be + // done synchronously. To work around this issue, we always call + // preventDefault, which means it is never logged but we rethrow the error + // after annotating its traceback, which will cause the error to be handled + // again after the traceback has been annotated, and this function will be + // called again and return synchronously (see above) + if (error.errorEvent) { + error.errorEvent.preventDefault(); + } + let traceback; + try { + traceback = await annotateTraceback(error); + let current = error.cause; + while (current) { + traceback += `\n\nCaused by: ${ + current instanceof Error ? await annotateTraceback(current) : current + }`; + current = current.cause; + } + } catch (e) { + console.warn("Failed to annotate traceback for error:", error, "failure reason:", e); + traceback = fullTraceback(error); + } + error.annotatedTraceback = traceback; + if (error.errorEvent) { + throw error; + } + return traceback; +} + +/** + * @param {UncaughtError} uncaughtError + * @param {Error} originalError + * @param {boolean} annotated + * @returns {Promise} + */ +export async function completeUncaughtError(uncaughtError, originalError, annotated = false) { + uncaughtError.name = combineErrorNames(uncaughtError, originalError); + if (annotated) { + uncaughtError.traceback = await fullAnnotatedTraceback(originalError); + } else { + uncaughtError.traceback = fullTraceback(originalError); + } + if (originalError.message) { + uncaughtError.message = `${uncaughtError.message} > ${originalError.message}`; + } + uncaughtError.cause = originalError; +} + /** * @param {Error} error * @returns {string} @@ -23,12 +119,14 @@ export function formatTraceback(error) { let traceback = error.stack; const errorName = getErrorTechnicalName(error); if (!isBrowserChrome()) { - // transforms the stack into a chromium stack - // Chromium stack example: + // transforms the stack into a chromium stack by adding the error name + // to the stack and indenting the lines, eg: // Error: Mock: Can't write value // _onOpenFormView@http://localhost:8069/web/content/425-baf33f1/web.assets.js:1064:30 // ... - traceback = `${errorName}: ${error.message}\n${error.stack}`.replace(/\n/g, "\n "); + traceback = `${errorName}: ${error.message}\n${error.stack}` + .replace(/\n/g, "\n ") + .trim(); } else if (error.stack) { // Chromium stack starts with the error's name but the name is "Error" by default // so we replace it to have the error type name @@ -76,16 +174,12 @@ export async function annotateTraceback(error) { lines.splice(-1); } - // Chrome stacks contains some lines with (index 0) which apparently - // corresponds to some native functions (at least Promise.all). We need to - // ignore them because they will not correspond to a stackframe. - const skips = lines.filter((l) => l.includes("(index 0")).length; - const offset = lines.length - frames.length - skips; - let lineIndex = offset; + let lineIndex = 0; let frameIndex = 0; while (frameIndex < frames.length) { const line = lines[lineIndex]; - if (line.includes("(index 0)")) { + // skip lines that have no location information as they don't correspond to a frame + if (!line.match(/:\d+:\d+\)?$/)) { lineIndex++; continue; } diff --git a/addons/web/static/src/legacy/js/core/error_utils.js b/addons/web/static/src/legacy/js/core/error_utils.js deleted file mode 100644 index 6f7ccc60f51..00000000000 --- a/addons/web/static/src/legacy/js/core/error_utils.js +++ /dev/null @@ -1,76 +0,0 @@ -/** @odoo-module **/ - -import { loadJS } from "@web/core/assets"; -import BrowserDetection from "web.BrowserDetection"; -import { _t } from "web.core"; -/** - * Format the traceback of an error. Basically, we just add the error message - * in the traceback if necessary (Chrome already does it by default, but not - * other browser. yay for non standard APIs) - * - * @param {Error} error - * @returns {string} - */ -export function formatTraceback(error) { - // Error.prototype.stack is non-standard. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - // However, most engines provide an implementation. - // In particular, Chrome formats the contents of Error.stack - // https://v8.dev/docs/stack-trace-api#compatibility - const browserDetection = new BrowserDetection(); - if (browserDetection.isBrowserChrome()) { - return error.stack; - } else { - return `${_t("Error:")} ${error.message}\n${error.stack}`; - } -} - -/** -* Returns an annotated traceback from an error. This is asynchronous because -* it needs to fetch the sourcemaps for each script involved in the error, -* then compute the correct file/line numbers and add the information to the -* correct line. -* -* @param {Error} error -* @returns {Promise} -*/ -export async function annotateTraceback(error) { - const traceback = formatTraceback(error); - await loadJS('/web/static/lib/stacktracejs/stacktrace.js'); - // In Firefox, the error stack generated by anonymous code (example: invalid - // code in a template) is not compatible with the stacktrace lib. This code - // corrects the stack to make it compatible with the lib stacktrace. - if (error.stack) { - const regex = / line (\d*) > (Function):(\d*)/gm; - const subst = `:$1`; - error.stack = error.stack.replace(regex, subst); - } - // eslint-disable-next-line no-undef - const frames = await StackTrace.fromError(error); - const lines = traceback.split('\n'); - if (lines[lines.length-1].trim() === "") { - // firefox traceback have an empty line at the end - lines.splice(-1); - } - - // Chrome stacks contains some lines with (index 0) which apparently - // corresponds to some native functions (at least Promise.all). We need to - // ignore them because they will not correspond to a stackframe. - const skips = lines.filter(l => l.includes("(index 0")).length; - const offset = lines.length - frames.length - skips; - let lineIndex = offset; - let frameIndex = 0; - while (frameIndex < frames.length) { - const line = lines[lineIndex]; - if (line.includes("(index 0)")) { - lineIndex++; - continue; - } - const frame = frames[frameIndex]; - const info = ` (${frame.fileName}:${frame.lineNumber})`; - lines[lineIndex] = line + info; - lineIndex++; - frameIndex++; - } - return lines.join('\n'); -} diff --git a/addons/web/static/src/legacy/legacy_rpc_error_handler.js b/addons/web/static/src/legacy/legacy_rpc_error_handler.js index 5073306116e..a6a0ddefbfe 100644 --- a/addons/web/static/src/legacy/legacy_rpc_error_handler.js +++ b/addons/web/static/src/legacy/legacy_rpc_error_handler.js @@ -4,6 +4,8 @@ import { registry } from "@web/core/registry"; import { ConnectionLostError, RPCError } from "../core/network/rpc_service"; import { lostConnectionHandler, rpcErrorHandler } from "@web/core/errors/error_handlers"; +const { OwlError } = owl; + const errorHandlerRegistry = registry.category("error_handlers"); /** @@ -22,6 +24,9 @@ const errorHandlerRegistry = registry.category("error_handlers"); * @returns {boolean} */ function legacyRPCErrorHandler(env, error, originalError) { + if (originalError instanceof OwlError) { + originalError = originalError.cause; + } if ( originalError && originalError.legacy && 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 f70bf75a310..012425719a6 100644 --- a/addons/web/static/tests/core/errors/error_service_tests.js +++ b/addons/web/static/tests/core/errors/error_service_tests.js @@ -41,10 +41,16 @@ QUnit.module("Error Service", { const windowAddEventListener = browser.addEventListener; browser.addEventListener = (type, cb) => { if (type === "unhandledrejection") { - unhandledRejectionCb = cb; + unhandledRejectionCb = (ev) => { + ev.preventDefault(); + cb(ev); + }; } if (type === "error") { - errorCb = cb; + errorCb = (ev) => { + ev.preventDefault(); + cb(ev); + }; } }; registerCleanup(() => { @@ -77,7 +83,11 @@ QUnit.test("handle RPC_ERROR of type='server' and no associated dialog class", a } serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true }); await makeTestEnv(); - const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: error, + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); }); @@ -112,7 +122,11 @@ QUnit.test( serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true }); await makeTestEnv(); errorDialogRegistry.add("strange_error", CustomDialog); - const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: error, + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); } ); @@ -152,7 +166,11 @@ QUnit.test( await makeTestEnv(); errorDialogRegistry.add("strange_error", CustomDialog); errorDialogRegistry.add("normal_error", NormalDialog); - const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: error, + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); } ); @@ -187,7 +205,11 @@ QUnit.test("handle CONNECTION_LOST_ERROR", async (assert) => { }; await makeTestEnv({ mockRPC }); const error = new ConnectionLostError(); - const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: error, + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); await nextTick(); // wait for mocked RPCs assert.verifySteps([ @@ -211,7 +233,11 @@ QUnit.test("will let handlers from the registry handle errors first", async (ass testEnv.someValue = 14; const error = new Error(); error.name = "boom"; - const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: error, + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); assert.verifySteps(["in handler"]); }); @@ -233,7 +259,11 @@ QUnit.test("handle uncaught promise errors", async (assert) => { serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true }); await makeTestEnv(); - const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: error, + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); }); @@ -256,6 +286,7 @@ QUnit.test("handle uncaught client errors", async (assert) => { colno: 1, lineno: 1, filename: "test", + cancelable: true, }); await errorCb(errorEvent); }); @@ -274,7 +305,7 @@ QUnit.test("handle uncaught CORS errors", async (assert) => { await makeTestEnv(); // CORS error event has no colno, no lineno and no filename - const errorEvent = new ErrorEvent("error", { error }); + const errorEvent = new ErrorEvent("error", { error, cancelable: true }); await errorCb(errorEvent); }); @@ -300,7 +331,11 @@ QUnit.test("check retry", async (assert) => { error.message = "This is an error test"; error.name = "TestError"; - const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: error, + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); assert.verifySteps([]); @@ -314,7 +349,11 @@ QUnit.test("check retry", async (assert) => { QUnit.test("lazy loaded handlers", async (assert) => { await makeTestEnv(); - const errorEvent = new PromiseRejectionEvent("error", { reason: new Error(), promise: null }); + const errorEvent = new PromiseRejectionEvent("error", { + reason: new Error(), + promise: null, + cancelable: true, + }); await unhandledRejectionCb(errorEvent); assert.verifySteps([]); diff --git a/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js b/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js index c7a461104b7..ccd8c38ccdc 100644 --- a/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js +++ b/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js @@ -77,6 +77,8 @@ QUnit.test("hotkey handles wrongly formed KeyboardEvent", async (assert) => { assert.step("error"); }; + // 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("error", handler); const _onError = window.onerror; window.onerror = () => {}; 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 8c3d9b933a4..ead78bab787 100644 --- a/addons/web/static/tests/core/main_components_container_tests.js +++ b/addons/web/static/tests/core/main_components_container_tests.js @@ -71,6 +71,8 @@ QUnit.module("Components", (hooks) => { 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: () => {} }); patch(QUnit, "MainComponentsContainer QUnit patch", { onUnhandledRejection: () => {}, }); @@ -124,6 +126,8 @@ QUnit.module("Components", (hooks) => { 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: () => {} }); patch(QUnit, "MainComponentsContainer QUnit patch", { onUnhandledRejection: () => {}, }); diff --git a/addons/web/static/tests/qunit.js b/addons/web/static/tests/qunit.js index 05fafb8919f..ee55be1187a 100644 --- a/addons/web/static/tests/qunit.js +++ b/addons/web/static/tests/qunit.js @@ -1,6 +1,13 @@ /** @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 { registry } from "@web/core/registry"; function setQUnitDebugMode() { owl.whenReady(() => document.body.classList.add("debug")); // make the test visible to the naked eye @@ -285,18 +292,19 @@ export function setupQUnit() { if (!result.failed && allModulesLoaded) { console.log("test successful"); } else { - console.error(errorMessages.join("\n")); - errorMessages = [] + console.error((await Promise.all(errorMessages)).join("\n")); + errorMessages = []; } }); /** * This is done mostly for the .txt log file generated by the runbot. */ - QUnit.moduleDone((result) => { + QUnit.moduleDone(async (result) => { if (errorMessages.length > 0) { - console.error(errorMessages.join("\n")); - errorMessages = [] + const messages = errorMessages.slice(); + errorMessages = []; + console.error((await Promise.all(messages)).join("\n")); } if (!result.failed) { console.log('"' + result.name + '"', "passed", result.total, "tests."); @@ -320,16 +328,20 @@ export function setupQUnit() { if (result.result) { return; } - let info = `QUnit test failed: ${result.module} > ${result.name} :`; - info += `\n\tmessage: "${result.message}"`; - if (result.expected !== null) { - info += `\n\texpected: "${result.expected}"`; - } - if (result.actual !== null) { - info += `\n\tactual: "${result.actual}"`; - } - - errorMessages.push(info); + errorMessages.push( + Promise.resolve(result.annotateProm).then(() => { + let info = `QUnit test failed: ${result.module} > ${result.name} :`; + info += `\n\tmessage: "${result.message}"`; + if ("expected" in result) { + info += `\n\texpected: "${result.expected}"`; + } + if (result.actual !== null) { + info += `\n\tactual: "${result.actual}"`; + } + info += `\n${result.source.replace(/^/gm, "\t")}\n`; + return info; + }) + ); }); // Append a "Rerun in debug" link. @@ -396,10 +408,10 @@ export function setupQUnit() { }); QUnit.begin(function () { - if (odoo.__DEBUG__.services["@web/core/errors/error_utils"]) { - const errorUtils = odoo.__DEBUG__.services["@web/core/errors/error_utils"]; - const { annotateTraceback } = errorUtils; - QUnit.annotateTraceback = annotateTraceback; + if (odoo.debug && odoo.debug.includes("assets")) { + QUnit.annotateTraceback = fullAnnotatedTraceback; + } else { + QUnit.annotateTraceback = (err) => Promise.resolve(fullTraceback(err)); } const config = QUnit.config; if (config.failfast) { @@ -515,4 +527,42 @@ export function setupQUnit() { el.innerText = "details:not([open]) > :not(summary) { display: none; }"; document.head.appendChild(el); }); + + const { onUnhandledRejection, onError } = QUnit; + QUnit.onUnhandledRejection = () => {}; + QUnit.onError = () => {}; + 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) { + 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); + }); + + 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) { + 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); + }); } 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 29f3f405bf5..c09014059d7 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -11968,6 +11968,8 @@ QUnit.module("Views", (hooks) => { // 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, { 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 e4e346f07c8..d04f64eaa49 100644 --- a/addons/web/static/tests/webclient/actions/close_action_tests.js +++ b/addons/web/static/tests/webclient/actions/close_action_tests.js @@ -11,6 +11,7 @@ import { } 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"; @@ -193,6 +194,8 @@ QUnit.module("ActionManager", (hooks) => { // 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, {