661 lines
25 KiB
JavaScript
661 lines
25 KiB
JavaScript
|
/** @odoo-module */
|
||
|
|
||
|
import { isVisible as isElemVisible } from "@web/core/utils/ui";
|
||
|
import { fullTraceback, fullAnnotatedTraceback } from "@web/core/errors/error_utils";
|
||
|
import { registry } from "@web/core/registry";
|
||
|
import { Component, whenReady } from "@odoo/owl";
|
||
|
|
||
|
const consoleError = console.error;
|
||
|
|
||
|
function setQUnitDebugMode() {
|
||
|
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...)
|
||
|
QUnit.config.testTimeout = 60 * 60 * 1000;
|
||
|
// Allows for interacting with the test when it is over
|
||
|
// In fact, this will pause QUnit.
|
||
|
// Also, logs useful info in the console.
|
||
|
QUnit.testDone(async (...args) => {
|
||
|
console.groupCollapsed("Debug Test output");
|
||
|
console.log(...args);
|
||
|
console.groupEnd();
|
||
|
await new Promise(() => {});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// need to do this outside of the setup function so the QUnit.debug is defined when we need it
|
||
|
QUnit.debug = (name, cb) => {
|
||
|
setQUnitDebugMode();
|
||
|
QUnit.only(name, cb);
|
||
|
};
|
||
|
|
||
|
// need to do this outside of the setup function so it is executed quickly
|
||
|
QUnit.config.autostart = false;
|
||
|
|
||
|
export function setupQUnit() {
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// QUnit config
|
||
|
// -----------------------------------------------------------------------------
|
||
|
QUnit.config.testTimeout = 1 * 60 * 1000;
|
||
|
QUnit.config.hidepassed = window.location.href.match(/[?&]testId=/) === null;
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// QUnit assert
|
||
|
// -----------------------------------------------------------------------------
|
||
|
/**
|
||
|
* Checks that the target contains exactly n matches for the selector.
|
||
|
*
|
||
|
* Example: assert.containsN(document.body, '.modal', 0)
|
||
|
*/
|
||
|
function containsN(target, selector, n, msg) {
|
||
|
let $el;
|
||
|
if (target._widgetRenderAndInsert) {
|
||
|
$el = target.$el; // legacy widget
|
||
|
} else if (target instanceof Component) {
|
||
|
if (!target.el) {
|
||
|
throw new Error(
|
||
|
`containsN assert with selector '${selector}' called on an unmounted component`
|
||
|
);
|
||
|
}
|
||
|
$el = $(target.el);
|
||
|
} else {
|
||
|
$el = target instanceof Element ? $(target) : target;
|
||
|
}
|
||
|
msg = msg || `Selector '${selector}' should have exactly ${n} matches inside the target`;
|
||
|
QUnit.assert.strictEqual($el.find(selector).length, n, msg);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks that the target contains exactly 0 match for the selector.
|
||
|
*
|
||
|
* @param {Element} el
|
||
|
* @param {string} selector
|
||
|
* @param {string} [msg]
|
||
|
*/
|
||
|
function containsNone(target, selector, msg) {
|
||
|
containsN(target, selector, 0, msg);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks that the target contains exactly 1 match for the selector.
|
||
|
*
|
||
|
* @param {Element} el
|
||
|
* @param {string} selector
|
||
|
* @param {string} [msg]
|
||
|
*/
|
||
|
function containsOnce(target, selector, msg) {
|
||
|
containsN(target, selector, 1, msg);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper function, to check if a given element has (or has not) classnames.
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Element | jQuery | Widget} el
|
||
|
* @param {string} classNames
|
||
|
* @param {boolean} shouldHaveClass
|
||
|
* @param {string} [msg]
|
||
|
*/
|
||
|
function _checkClass(el, classNames, shouldHaveClass, msg) {
|
||
|
if (el) {
|
||
|
if (el._widgetRenderAndInsert) {
|
||
|
el = el.el; // legacy widget
|
||
|
} else if (!(el instanceof Element)) {
|
||
|
el = el[0];
|
||
|
}
|
||
|
}
|
||
|
msg =
|
||
|
msg ||
|
||
|
`target should ${shouldHaveClass ? "have" : "not have"} classnames ${classNames}`;
|
||
|
const isFalse = classNames.split(" ").some((cls) => {
|
||
|
const hasClass = el.classList.contains(cls);
|
||
|
return shouldHaveClass ? !hasClass : hasClass;
|
||
|
});
|
||
|
QUnit.assert.ok(!isFalse, msg);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks that the target element has the given classnames.
|
||
|
*
|
||
|
* @param {Element} el
|
||
|
* @param {string} classNames
|
||
|
* @param {string} [msg]
|
||
|
*/
|
||
|
function hasClass(el, classNames, msg) {
|
||
|
_checkClass(el, classNames, true, msg);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks that the target element does not have the given classnames.
|
||
|
*
|
||
|
* @param {Element} el
|
||
|
* @param {string} classNames
|
||
|
* @param {string} [msg]
|
||
|
*/
|
||
|
function doesNotHaveClass(el, classNames, msg) {
|
||
|
_checkClass(el, classNames, false, msg);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks that the target element (described by widget/jquery or html element)
|
||
|
* - exists
|
||
|
* - is unique
|
||
|
* - has the given attribute with the proper value
|
||
|
*
|
||
|
* @param {Component | Element | Widget | jQuery} w
|
||
|
* @param {string} attr
|
||
|
* @param {string} value
|
||
|
* @param {string} [msg]
|
||
|
*/
|
||
|
function hasAttrValue(target, attr, value, msg) {
|
||
|
let $el;
|
||
|
if (target._widgetRenderAndInsert) {
|
||
|
$el = target.$el; // legacy widget
|
||
|
} else if (target instanceof Component) {
|
||
|
if (!target.el) {
|
||
|
throw new Error(
|
||
|
`hasAttrValue assert with attr '${attr}' called on an unmounted component`
|
||
|
);
|
||
|
}
|
||
|
$el = $(target.el);
|
||
|
} else {
|
||
|
$el = target instanceof Element ? $(target) : target;
|
||
|
}
|
||
|
|
||
|
if ($el.length !== 1) {
|
||
|
const descr = `hasAttrValue (${attr}: ${value})`;
|
||
|
QUnit.assert.ok(
|
||
|
false,
|
||
|
`Assertion '${descr}' targets ${$el.length} elements instead of 1`
|
||
|
);
|
||
|
} else {
|
||
|
msg = msg || `attribute '${attr}' of target should be '${value}'`;
|
||
|
QUnit.assert.strictEqual($el.attr(attr), value, msg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper function, to check if a given element
|
||
|
* - is unique (if it is a jquery node set)
|
||
|
* - is (or not) visible
|
||
|
*
|
||
|
* @private
|
||
|
* @param {Element | jQuery | Widget} el
|
||
|
* @param {boolean} shouldBeVisible
|
||
|
* @param {string} [msg]
|
||
|
*/
|
||
|
function _checkVisible(el, shouldBeVisible, msg) {
|
||
|
if (el) {
|
||
|
if (el._widgetRenderAndInsert) {
|
||
|
el = el.el; // legacy widget
|
||
|
} else if (!(el instanceof Element)) {
|
||
|
el = el[0];
|
||
|
}
|
||
|
}
|
||
|
msg = msg || `target should ${shouldBeVisible ? "" : "not"} be visible`;
|
||
|
const _isVisible = isElemVisible(el);
|
||
|
const condition = shouldBeVisible ? _isVisible : !_isVisible;
|
||
|
QUnit.assert.ok(condition, msg);
|
||
|
}
|
||
|
function isVisible(el, msg) {
|
||
|
return _checkVisible(el, true, msg);
|
||
|
}
|
||
|
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;
|
||
|
QUnit.assert.doesNotHaveClass = doesNotHaveClass;
|
||
|
QUnit.assert.hasClass = hasClass;
|
||
|
QUnit.assert.hasAttrValue = hasAttrValue;
|
||
|
QUnit.assert.isVisible = isVisible;
|
||
|
QUnit.assert.isNotVisible = isNotVisible;
|
||
|
QUnit.assert.expectErrors = expectErrors;
|
||
|
QUnit.assert.verifyErrors = verifyErrors;
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// QUnit logs
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* If we want to log several errors, we have to log all of them at once, as
|
||
|
* browser_js is closed as soon as an error is logged.
|
||
|
*/
|
||
|
let errorMessages = [];
|
||
|
async function logErrors() {
|
||
|
const messages = errorMessages.slice();
|
||
|
errorMessages = [];
|
||
|
const infos = await Promise.all(messages);
|
||
|
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(
|
||
|
infos
|
||
|
.map((info) =>
|
||
|
info.source ? `${info.error}\n${info.source.replace(/^/gm, "\t")}\n` : info
|
||
|
)
|
||
|
.join("\n")
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If we want to log several errors, we have to log all of them at once, as
|
||
|
* browser_js is closed as soon as an error is logged.
|
||
|
*/
|
||
|
QUnit.done(async (result) => {
|
||
|
await odoo.loader.checkErrorProm;
|
||
|
const moduleLoadingError = document.querySelector(".o_module_error");
|
||
|
if (moduleLoadingError) {
|
||
|
errorMessages.unshift(moduleLoadingError.innerText);
|
||
|
}
|
||
|
if (result.failed) {
|
||
|
errorMessages.push(`${result.failed} / ${result.total} tests failed.`);
|
||
|
}
|
||
|
if (!result.failed && !moduleLoadingError) {
|
||
|
console.log("QUnit test suite done.");
|
||
|
console.log("test successful"); // for ChromeBowser to know it's over and ok
|
||
|
} else {
|
||
|
logErrors();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* This is done mostly for the .txt log file generated by the runbot.
|
||
|
*/
|
||
|
QUnit.moduleDone(async (result) => {
|
||
|
if (!result.failed) {
|
||
|
console.log('"' + result.name + '"', "passed", result.total, "tests.");
|
||
|
} else {
|
||
|
console.log(
|
||
|
'"' + result.name + '"',
|
||
|
"failed",
|
||
|
result.failed,
|
||
|
"tests out of",
|
||
|
result.total,
|
||
|
"."
|
||
|
);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* This logs various data in the console, which will be available in the log
|
||
|
* .txt file generated by the runbot.
|
||
|
*/
|
||
|
QUnit.log((result) => {
|
||
|
if (result.result) {
|
||
|
return;
|
||
|
}
|
||
|
errorMessages.push(
|
||
|
Promise.resolve(result.annotateProm).then(() => {
|
||
|
let info = `QUnit test failed: ${result.module} > ${result.name} :`;
|
||
|
if (result.message) {
|
||
|
info += `\n\tmessage: "${result.message}"`;
|
||
|
}
|
||
|
if ("expected" in result) {
|
||
|
info += `\n\texpected: "${result.expected}"`;
|
||
|
}
|
||
|
if (result.actual !== null) {
|
||
|
info += `\n\tactual: "${result.actual}"`;
|
||
|
}
|
||
|
return {
|
||
|
error: info,
|
||
|
source: result.source,
|
||
|
};
|
||
|
})
|
||
|
);
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* The purpose of this function is to reset the timer nesting level of the execution context
|
||
|
* to 0, to prevent situations where a setTimeout with a timeout of 0 may end up being
|
||
|
* scheduled after another one that also has a timeout of 0 that was called later.
|
||
|
* Example code:
|
||
|
* (async () => {
|
||
|
* const timeout = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||
|
* const animationFrame = () => new Promise((resolve) => requestAnimationFrame(resolve));
|
||
|
*
|
||
|
* for (let i = 0; i < 4; i++) {
|
||
|
* await timeout();
|
||
|
* }
|
||
|
* timeout().then(() => console.log("after timeout"));
|
||
|
* await animationFrame()
|
||
|
* timeout().then(() => console.log("after animationFrame"));
|
||
|
* // logs "after animationFrame" before "after timeout"
|
||
|
* })()
|
||
|
*
|
||
|
* When the browser runs a task that was the result of a timer (setTimeout or setInterval),
|
||
|
* that task has an intrinsic "timer nesting level". If you schedule another task with
|
||
|
* a timer from within such a task, the new task has the existing task's timer nesting level,
|
||
|
* plus one. When the timer nesting level of a task is greater than 5, the `timeout` parameter
|
||
|
* for setTimeout/setInterval will be forced to at least 4 (see step 5 in the timer initialization
|
||
|
* steps in the HTML spec: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps).
|
||
|
*
|
||
|
* In the above example, every `await timeout()` besides inside the loop schedules a new task
|
||
|
* from within a task that was initiated by a timer, causing the nesting level to be 5 after
|
||
|
* the loop. The first timeout after the loop is now forced to 4.
|
||
|
*
|
||
|
* When we await the animation frame promise, we create a task that is *not* initiated by a timer,
|
||
|
* reseting the nesting level to 0, causing the timeout following it to properly be treated as 0,
|
||
|
* as such the callback that was registered by it is oftentimes executed before the previous one.
|
||
|
*
|
||
|
* While we can't prevent this from happening within a given test, we want to at least prevent
|
||
|
* the timer nesting level to propagate from one test to the next as this can be a cause of
|
||
|
* indeterminism. To avoid slowing down the tests by waiting one frame after every test,
|
||
|
* we instead use a MessageChannel to add a task with not nesting level to the event queue immediately.
|
||
|
*/
|
||
|
QUnit.testDone(async () => {
|
||
|
return new Promise((resolve) => {
|
||
|
const channel = new MessageChannel();
|
||
|
channel.port1.onmessage = () => {
|
||
|
channel.port1.close();
|
||
|
channel.port2.close();
|
||
|
resolve();
|
||
|
};
|
||
|
channel.port2.postMessage("");
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Append a "Rerun in debug" link.
|
||
|
// Only works if the test is not hidden.
|
||
|
QUnit.testDone(async ({ testId }) => {
|
||
|
if (errorMessages.length > 0) {
|
||
|
logErrors();
|
||
|
}
|
||
|
const testElement = document.getElementById(`qunit-test-output-${testId}`);
|
||
|
if (!testElement) {
|
||
|
// Is probably hidden because it passed
|
||
|
return;
|
||
|
}
|
||
|
const reRun = testElement.querySelector("li a");
|
||
|
const reRunDebug = document.createElement("a");
|
||
|
reRunDebug.textContent = "Rerun in debug";
|
||
|
const url = new URL(window.location);
|
||
|
url.searchParams.set("testId", testId);
|
||
|
url.searchParams.set("debugTest", "true");
|
||
|
reRunDebug.setAttribute("href", url.href);
|
||
|
reRun.parentElement.insertBefore(reRunDebug, reRun.nextSibling);
|
||
|
});
|
||
|
|
||
|
const debugTest = new URLSearchParams(location.search).get("debugTest");
|
||
|
if (debugTest) {
|
||
|
setQUnitDebugMode();
|
||
|
}
|
||
|
|
||
|
// Override global UnhandledRejection that is assigned wayyy before this file
|
||
|
// Do not really crash on non-errors rejections
|
||
|
const qunitUnhandledReject = QUnit.onUnhandledRejection;
|
||
|
QUnit.onUnhandledRejection = (reason) => {
|
||
|
const error = reason instanceof Error && "cause" in reason ? reason.cause : reason;
|
||
|
if (error instanceof Error) {
|
||
|
qunitUnhandledReject(reason);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Essentially prevents default error logging when the rejection was
|
||
|
// not due to an actual error
|
||
|
const windowUnhandledReject = window.onunhandledrejection;
|
||
|
window.onunhandledrejection = (ev) => {
|
||
|
const error =
|
||
|
ev.reason instanceof Error && "cause" in ev.reason ? ev.reason.cause : ev.reason;
|
||
|
if (!(error instanceof Error)) {
|
||
|
ev.stopImmediatePropagation();
|
||
|
ev.preventDefault();
|
||
|
} else if (windowUnhandledReject) {
|
||
|
windowUnhandledReject.call(window, ev);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// FailFast
|
||
|
// -----------------------------------------------------------------------------
|
||
|
/**
|
||
|
* We add here a 'fail fast' feature: we often want to stop the test suite after
|
||
|
* the first failed test. This is also useful for the runbot test suites.
|
||
|
*/
|
||
|
QUnit.config.urlConfig.push({
|
||
|
id: "failfast",
|
||
|
label: "Fail Fast",
|
||
|
tooltip: "Stop the test suite immediately after the first failed test.",
|
||
|
});
|
||
|
|
||
|
QUnit.begin(function () {
|
||
|
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) {
|
||
|
QUnit.testDone(function (details) {
|
||
|
if (details.failed > 0) {
|
||
|
config.queue.length = 0;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Add sort button
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
let sortButtonAppended = false;
|
||
|
/**
|
||
|
* Add a sort button on top of the QUnit result page, so we can see which tests
|
||
|
* take the most time.
|
||
|
*/
|
||
|
function addSortButton() {
|
||
|
sortButtonAppended = true;
|
||
|
var $sort = $("<label> sort by time (desc)</label>").css({ float: "right" });
|
||
|
$("h2#qunit-userAgent").append($sort);
|
||
|
$sort.click(function () {
|
||
|
var $ol = $("ol#qunit-tests");
|
||
|
var $results = $ol.children("li").get();
|
||
|
$results.sort(function (a, b) {
|
||
|
var timeA = Number($(a).find("span.runtime").first().text().split(" ")[0]);
|
||
|
var timeB = Number($(b).find("span.runtime").first().text().split(" ")[0]);
|
||
|
if (timeA < timeB) {
|
||
|
return 1;
|
||
|
} else if (timeA > timeB) {
|
||
|
return -1;
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
});
|
||
|
$.each($results, function (idx, $itm) {
|
||
|
$ol.append($itm);
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
QUnit.done(() => {
|
||
|
if (!sortButtonAppended) {
|
||
|
addSortButton();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// Add statistics
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
let passedEl;
|
||
|
let failedEl;
|
||
|
let skippedEl;
|
||
|
let todoCompletedEl;
|
||
|
let todoUncompletedEl;
|
||
|
function insertStats() {
|
||
|
const toolbar = document.querySelector("#qunit-testrunner-toolbar .qunit-url-config");
|
||
|
const statsEl = document.createElement("label");
|
||
|
passedEl = document.createElement("span");
|
||
|
passedEl.classList.add("text-success", "ms-5", "me-3");
|
||
|
statsEl.appendChild(passedEl);
|
||
|
todoCompletedEl = document.createElement("span");
|
||
|
todoCompletedEl.classList.add("text-warning", "me-3");
|
||
|
statsEl.appendChild(todoCompletedEl);
|
||
|
failedEl = document.createElement("span");
|
||
|
failedEl.classList.add("text-danger", "me-3");
|
||
|
statsEl.appendChild(failedEl);
|
||
|
todoUncompletedEl = document.createElement("span");
|
||
|
todoUncompletedEl.classList.add("text-primary", "me-3");
|
||
|
statsEl.appendChild(todoUncompletedEl);
|
||
|
skippedEl = document.createElement("span");
|
||
|
skippedEl.classList.add("text-dark");
|
||
|
statsEl.appendChild(skippedEl);
|
||
|
toolbar.appendChild(statsEl);
|
||
|
}
|
||
|
|
||
|
let testPassedCount = 0;
|
||
|
let testFailedCount = 0;
|
||
|
let testSkippedCount = 0;
|
||
|
let todoCompletedCount = 0;
|
||
|
let todoUncompletedCount = 0;
|
||
|
QUnit.testDone(({ skipped, failed, todo }) => {
|
||
|
if (!passedEl) {
|
||
|
insertStats();
|
||
|
}
|
||
|
if (!skipped) {
|
||
|
if (failed > 0) {
|
||
|
if (todo) {
|
||
|
todoUncompletedCount++;
|
||
|
} else {
|
||
|
testFailedCount++;
|
||
|
}
|
||
|
} else {
|
||
|
if (todo) {
|
||
|
todoCompletedCount++;
|
||
|
} else {
|
||
|
testPassedCount++;
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
testSkippedCount++;
|
||
|
}
|
||
|
passedEl.innerText = `${testPassedCount} passed`;
|
||
|
if (todoCompletedCount > 0) {
|
||
|
todoCompletedEl.innerText = `${todoCompletedCount} todo completed`;
|
||
|
}
|
||
|
if (todoUncompletedCount > 0) {
|
||
|
todoUncompletedEl.innerText = `${todoUncompletedCount} todo uncompleted`;
|
||
|
}
|
||
|
if (testFailedCount > 0) {
|
||
|
failedEl.innerText = `${testFailedCount} failed`;
|
||
|
}
|
||
|
if (testSkippedCount > 0) {
|
||
|
skippedEl.innerText = `${testSkippedCount} skipped`;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// FIXME: This sounds stupid, it feels stupid... but it fixes visibility check in folded <details> since Chromium 97+ 💩
|
||
|
// Since https://bugs.chromium.org/p/chromium/issues/detail?id=1185950
|
||
|
// See regression report https://bugs.chromium.org/p/chromium/issues/detail?id=1276028
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
QUnit.begin(() => {
|
||
|
const el = document.createElement("style");
|
||
|
el.innerText = "details:not([open]) > :not(summary) { display: none; }";
|
||
|
document.head.appendChild(el);
|
||
|
});
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
// 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) => {
|
||
|
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 (
|
||
|
ev.message === "ResizeObserver loop limit exceeded" ||
|
||
|
ev.message === "ResizeObserver loop completed with undelivered notifications."
|
||
|
) {
|
||
|
return;
|
||
|
}
|
||
|
onUncaughtErrorInTest(ev.error);
|
||
|
});
|
||
|
|
||
|
// e.g. Promise.resolve().then(() => throw new Error()) (crash in event handler after async boundary)
|
||
|
window.addEventListener("unhandledrejection", async (ev) => {
|
||
|
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;
|
||
|
}
|
||
|
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,
|
||
|
});
|
||
|
}
|