[FIX] web: crash when clicking deleted records in breadcrumbs

Here is a way to reproduce the problematic situation: with CRM
installed, go to Contacts, open one, clicks on its Opportunities
stat button, open one in form view, click on the "Customer" field
internal link to open again the contact in form view, delete the
record, and finally click on the first occurence of the contact
in the breadcrumbs.

Before this commit, it crashed because we ended up in an error
handler (formSaveErrorHandler) that doesn't deal with the fact
that originalError could be falsy. This is handled by the
legacyRejectPromiseHandler, but due to the low sequence of the
faulty handler, this one was executed after.

Same error would occur if you tried to open an url of a record
that doesn't exist.

This commit fixes the error handler sequences s.t. the crash no
longer occurs.

Issue reported in the feedback pad after migrating odoo.com to 16.0.

closes odoo/odoo#110545

X-original-commit: 8e78efa3dda2484b34422a66d3963c7b20915036
Signed-off-by: Samuel Degueldre <sad@odoo.com>
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Aaron Bohy
2023-01-20 21:56:53 +01:00
parent a10967214b
commit a7a7c150aa
5 changed files with 78 additions and 5 deletions
@@ -18,7 +18,7 @@ import { registry } from "@web/core/registry";
* @returns {boolean}
*/
function legacyRejectPromiseHandler(env, error, originalError) {
if (error.name === "UncaughtPromiseError") {
if (error.name.startsWith("UncaughtPromiseError")) {
const isLegitError = originalError && originalError instanceof Error;
const isLegacyRPC = originalError && originalError.legacy;
if (!isLegitError && !isLegacyRPC) {
@@ -44,4 +44,4 @@ function legacyRPCErrorHandler(env, error, originalError) {
}
return false;
}
errorHandlerRegistry.add("legacyRPCErrorHandler", legacyRPCErrorHandler, { sequence: 2 });
errorHandlerRegistry.add("legacyRPCErrorHandler", legacyRPCErrorHandler, { sequence: 97 });
@@ -55,4 +55,4 @@ function formSaveErrorHandler(env, error, originalError) {
return true;
}
}
errorHandlerRegistry.add("formSaveErrorHandler", formSaveErrorHandler, { sequence: 1 });
errorHandlerRegistry.add("formSaveErrorHandler", formSaveErrorHandler);
+2 -2
View File
@@ -559,8 +559,8 @@ export function setupQUnit() {
if (originalError instanceof Error) {
originalError.errorEvent = ev;
await completeUncaughtError(uncaughtError, originalError);
originalError.stacktrace = uncaughtError.traceback;
}
originalError.stacktrace = uncaughtError.traceback;
onError(originalError);
});
@@ -577,8 +577,8 @@ export function setupQUnit() {
if (originalError instanceof Error) {
originalError.errorEvent = ev;
await completeUncaughtError(uncaughtError, originalError);
originalError.stack = uncaughtError.traceback;
}
originalError.stack = uncaughtError.traceback;
onUnhandledRejection(originalError);
});
}
@@ -2631,4 +2631,77 @@ QUnit.module("ActionManager", (hooks) => {
assert.containsOnce(target, ".o_pivot_view .o_view_sample_data");
});
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"/>
</form>`;
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
await click(target.querySelector(".my_btn"));
assert.containsOnce(target, ".o_list_view");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".breadcrumb-item")), [
"Partners",
"First record",
"Partners",
"First record",
]);
// open action menu and delete
await cpHelpers.toggleActionMenu(target);
await cpHelpers.toggleMenuItem(target, "Delete");
assert.containsOnce(target, ".o_dialog");
// confirm
await click(target.querySelector(".o_dialog .modal-footer .btn-primary"));
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".breadcrumb-item")), [
"Partners",
"First record",
"Partners",
"Second record",
]);
// click on "First record" in breadcrumbs, which doesn't exist anymore
await click(target.querySelectorAll(".breadcrumb-item a")[1]);
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".breadcrumb-item")), [
"Partners",
"First record",
"Partners",
"Second record",
]);
});
});