[IMP] web,survey: better error management in tests

The initial motivation of this commit was to ensure that the qunit
test suite doesn't stop when an error is thrown in a test, which
could happen if the error was thrown "sufficiently close to the
end of the test". Indeed, the "unhandledrejection" event being
async, it was sometimes triggered after the end of the test, when
the service registry was already reset, and the check of the
presence of the error service was wrong, so the error event wasn't
default prevented (e.g. await makeView(...) and the view crashes
at render time).

This led us to rework in more depth the way we deal with errors in
tests. Here are a few behaviors we want (probably not exhaustive):
 - an error in a test must never end the suite (executed in py)
 - an error in a test must always make the test fail, except if the
   error is expected in the scenario, which one must be able to
   state
 - a test must always wait for potential unhandledrejection events
   to be triggered before ending.
 - ideally, we don't want to have to deal with unhandledrejection
   in each test throwing an error (in order to prevent the suite to
   stop)

To achieve this, we come with the following solution. We introduce
a new assertion method, "expectToThrow" which allows to state that
during the test, we expect errors to be thrown. It takes a list of
error messages that will be compared at the end of the test with
the errors that have been thrown during the test. If they differ,
a qunit failure is pushed and the test fails. If an error occurs
in a test and "expectToThrow" hasn't been called, qunit is directly
informed of the error and a failing assertion is done, make the
test fail as well.

If the error service isn't available in the test environment, we
apply the logic above when an "error" or and "unhandledrejection"
event is thrown. If the error service is available, we wrap the
default handler (typically the one that handles everything that
hasn't been handled by specific handlers, like tracebacks) and if
we get to it, we apply the logic above. This means that one must
call "expectToThrow" if
 - the error service isn't deployed, or
 - the thrown error is handled by the default handler, because
   it is something like a traceback (errors like UserError,
   ValidationError are graciously handled by the RPCErrorHandler)
   and thus never reach the default handler.

In all cases, we prevent default the event such that the error
doesn't make the python test end.

Finally, to ensure that "undhandledrejection" events are handled
before the test ends, we wait, in the qunit lib, for a setTimeout
before ending the test, which ensures that all such events have
been dispatched.

closes odoo/odoo#137120

Related: odoo/enterprise#48211
Signed-off-by: Samuel Degueldre (sad) <sad@odoo.com>
This commit is contained in:
Aaron Bohy
2023-10-03 06:16:46 +00:00
parent 2331165bd0
commit 2f9c4979fa
19 changed files with 186 additions and 461 deletions
@@ -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",
+8 -2
View File
@@ -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,
@@ -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,
@@ -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) {
@@ -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`<CodeEditor value="props.value"/>`;
@@ -121,10 +110,8 @@ QUnit.module("Web Components", (hooks) => {
const textMarkup = markup`<div>Some Text</div>`;
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) => {
@@ -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"]);
});
@@ -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") {
@@ -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,
"<span>MainComponentA</span>"
+90 -33
View File
@@ -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,
});
}
@@ -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,
@@ -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": '<form><field name="name" /></form>',
};
@@ -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") {
@@ -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": `<form><field name="foo"/><footer><button type="object" name="some_method" class="myButton"/></footer></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: `<form><field name="foo" required="1"/></form>`,
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");
@@ -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) => {
</tree>`,
mockRPC(route, args) {
if (args.method === "web_save") {
throw new Error("Can't write");
throw makeServerError({ message: "Can't write" });
}
},
});
@@ -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(
@@ -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')"]);
});
});
@@ -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");
@@ -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");
@@ -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"] = `
<form>
<button type="action" name="3" string="Open Action 3" class="my_btn"/>
@@ -2440,29 +2373,6 @@ QUnit.module("ActionManager", (hooks) => {
});
QUnit.test("Uncaught error in target new is catch only once", 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() {