[IMP] web: display full tracebacks for error chains
Since owl now uses error chains/causes when errors happen in the owl lifecycle, when an error happens in the owl lifecycle, the displayed tracebacks generally only contain the stack trace of where owl called the corresponding lifecycle hook. This commit modifies the error service and the error utils so that now, when completing/annotating a traceback, we also add the tracebacks (annotated when appropriate) of the error cause chain, as it contains valuable debugging information. This commit also makes it so that the QUnit suite logs the source of each test failure (which may be an error with a chained stack trace) closes odoo/odoo#98157 Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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<string>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<string>}
|
||||
*/
|
||||
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');
|
||||
}
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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 = () => {};
|
||||
|
||||
@@ -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: () => {},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user