vanilla 17.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:47:08 +02:00
parent d72e748793
commit a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions

View file

@ -1,59 +1,21 @@
/** @odoo-module **/
import { _t } from "web.core";
import LegacyBus from "web.Bus";
import session from "web.session";
import { assets, templates } from "@web/core/assets";
import { browser, makeRAMLocalStorage } from "@web/core/browser/browser";
import { patchTimeZone, patchWithCleanup } from "@web/../tests/helpers/utils";
import { memoize } from "@web/core/utils/functions";
import { legacyProm } from "web.test_legacy";
import { registerCleanup } from "./helpers/cleanup";
import { utils } from "./helpers/mock_env";
import { prepareRegistriesWithCleanup } from "./helpers/mock_env";
import { session as sessionInfo } from "@web/session";
import { prepareLegacyRegistriesWithCleanup } from "./helpers/legacy_env_utils";
import { config as transitionConfig } from "@web/core/transition";
import { loadLanguages } from "@web/core/l10n/translation";
transitionConfig.disabled = true;
import { patch } from "@web/core/utils/patch";
import { App, whenReady } from "@odoo/owl";
const { prepareRegistriesWithCleanup } = utils;
function stringifyObjectValues(obj, properties) {
let res = "";
for (const dotted of properties) {
const keys = dotted.split(".");
let val = obj;
for (const k of keys) {
val = val[k];
}
res += JSON.stringify(val);
}
return res;
}
function checkGlobalObjectsIntegrity() {
const objects = [
[session, ["user_context", "currencies"]],
[_t, ["database.multi_lang", "database.parameters"]],
];
const initials = objects.map((obj) => stringifyObjectValues(obj[0], obj[1]));
registerCleanup((infos) => {
const finals = objects.map((obj) => stringifyObjectValues(obj[0], obj[1]));
for (const index in initials) {
if (initials[index] !== finals[index]) {
const [, /* global */ keys] = objects[index];
throw new Error(
`The keys "${keys}" of some global objects (usually session or _t) may have been polluted by the test "${infos.testName}" in module "${infos.moduleName}". Initial: ${initials[index]}. Final: ${finals[index]}.`
);
}
}
});
}
import { App, EventBus, whenReady } from "@odoo/owl";
import { currencies } from "@web/core/currency";
import { cookie } from "@web/core/browser/cookie";
function forceLocaleAndTimezoneWithCleanup() {
const originalLocale = luxon.Settings.defaultLocale;
@ -102,15 +64,31 @@ function patchOwlApp() {
patchWithCleanup(App.prototype, {
destroy() {
if (!this.destroyed) {
this._super(...arguments);
super.destroy(...arguments);
this.destroyed = true;
}
},
addTemplate(name) {
registerCleanup(() => {
delete this.constructor.sharedTemplates[name];
});
return this._super(...arguments);
});
}
function patchCookie() {
const cookieJar = {};
patchWithCleanup(cookie, {
get _cookieMonster() {
return Object.entries(cookieJar)
.filter(([, value]) => value !== "kill")
.map((cookie) => cookie.join("="))
.join("; ");
},
set _cookieMonster(value) {
const cookies = value.split("; ");
for (const cookie of cookies) {
const [key, value] = cookie.split(/=(.*)/);
if (!["path", "max-age"].includes(key)) {
cookieJar[key] = value;
}
}
},
});
}
@ -127,76 +105,84 @@ function patchBrowserWithCleanup() {
let nextAnimationFrameHandle = 1;
const animationFrameHandles = new Set();
const mockLocation = makeMockLocation(() => hasHashChangeListeners);
patchWithCleanup(
browser,
{
// patch addEventListner to automatically remove listeners bound (via
// browser.addEventListener) during a test (e.g. during the deployment of a service)
addEventListener(evName) {
if (evName === "hashchange") {
hasHashChangeListeners = true;
}
originalAddEventListener(...arguments);
registerCleanup(() => {
originalRemoveEventListener(...arguments);
});
},
// patch setTimeout to automatically remove timeouts bound (via
// browser.setTimeout) during a test (e.g. during the deployment of a service)
setTimeout() {
const timeout = originalSetTimeout(...arguments);
registerCleanup(() => {
originalClearTimeout(timeout);
});
return timeout;
},
// patch setInterval to automatically remove callbacks registered (via
// browser.setInterval) during a test (e.g. during the deployment of a service)
setInterval() {
const interval = originalSetInterval(...arguments);
registerCleanup(() => {
originalClearInterval(interval);
});
return interval;
},
navigator: {
userAgent: browser.navigator.userAgent.replace(/\([^)]*\)/, "(X11; Linux x86_64)"),
sendBeacon: () => {
throw new Error("sendBeacon called in test but not mocked");
},
},
// in tests, we never want to interact with the real url or reload the page
location: mockLocation,
history: {
pushState(state, title, url) {
mockLocation.assign(url);
},
replaceState(state, title, url) {
mockLocation.assign(url);
},
},
// in tests, we never want to interact with the real local/session storages.
localStorage: makeRAMLocalStorage(),
sessionStorage: makeRAMLocalStorage(),
// Don't want original animation frames in tests
requestAnimationFrame: (fn) => {
const handle = nextAnimationFrameHandle++;
animationFrameHandles.add(handle);
Promise.resolve().then(() => {
if (animationFrameHandles.has(handle)) {
fn(16);
}
});
return handle;
},
cancelAnimationFrame: (handle) => {
animationFrameHandles.delete(handle);
patchWithCleanup(browser, {
// patch addEventListner to automatically remove listeners bound (via
// browser.addEventListener) during a test (e.g. during the deployment of a service)
addEventListener(evName) {
if (evName === "hashchange") {
hasHashChangeListeners = true;
}
originalAddEventListener(...arguments);
registerCleanup(() => {
originalRemoveEventListener(...arguments);
});
},
// patch setTimeout to automatically remove timeouts bound (via
// browser.setTimeout) during a test (e.g. during the deployment of a service)
setTimeout() {
const timeout = originalSetTimeout(...arguments);
registerCleanup(() => {
originalClearTimeout(timeout);
});
return timeout;
},
// patch setInterval to automatically remove callbacks registered (via
// browser.setInterval) during a test (e.g. during the deployment of a service)
setInterval() {
const interval = originalSetInterval(...arguments);
registerCleanup(() => {
originalClearInterval(interval);
});
return interval;
},
// patch BeforeInstallPromptEvent to prevent the installPrompt service to return an uncontrolled
// canPromptToInstall value depending the browser settings (we ensure the value is always falsy)
BeforeInstallPromptEvent: undefined,
navigator: {
mediaDevices: browser.navigator.mediaDevices,
permissions: browser.navigator.permissions,
userAgent: browser.navigator.userAgent.replace(/\([^)]*\)/, "(X11; Linux x86_64)"),
sendBeacon: () => {
throw new Error("sendBeacon called in test but not mocked");
},
},
{ pure: true }
);
// in tests, we never want to interact with the real url or reload the page
location: mockLocation,
history: {
pushState(state, title, url) {
mockLocation.assign(url);
},
replaceState(state, title, url) {
mockLocation.assign(url);
},
},
// in tests, we never want to interact with the real local/session storages.
localStorage: makeRAMLocalStorage(),
sessionStorage: makeRAMLocalStorage(),
// Don't want original animation frames in tests
requestAnimationFrame: (fn) => {
const handle = nextAnimationFrameHandle++;
animationFrameHandles.add(handle);
Promise.resolve().then(() => {
if (animationFrameHandles.has(handle)) {
fn(16);
}
});
return handle;
},
cancelAnimationFrame: (handle) => {
animationFrameHandles.delete(handle);
},
// BroadcastChannels need to be closed to be garbage collected
BroadcastChannel: class SelfClosingBroadcastChannel extends BroadcastChannel {
constructor() {
super(...arguments);
registerCleanup(() => this.close());
}
},
});
}
function patchBodyAddEventListener() {
@ -216,19 +202,6 @@ function patchBodyAddEventListener() {
});
}
function patchLegacyBus() {
// patch core.bus.on to automatically remove listners bound on the legacy bus
// during a test (e.g. during the deployment of a service)
patchWithCleanup(LegacyBus.prototype, {
on() {
this._super(...arguments);
registerCleanup(() => {
this.off(...arguments);
});
},
});
}
function patchOdoo() {
patchWithCleanup(odoo, {
debug: "",
@ -247,10 +220,6 @@ function patchSessionInfo() {
load_menus: "161803",
translations: "314159",
},
currencies: {
1: { name: "USD", digits: [69, 2], position: "before", symbol: "$" },
2: { name: "EUR", digits: [69, 2], position: "after", symbol: "€" },
},
user_context: {
lang: "en",
uid: 7,
@ -271,7 +240,11 @@ function patchSessionInfo() {
},
db: "test",
server_version: "1.0",
server_version_info: ["1.0"],
server_version_info: [1, 0, 0, "final", 0, ""],
});
patchWithCleanup(currencies, {
1: { name: "USD", digits: [69, 2], position: "before", symbol: "$" },
2: { name: "EUR", digits: [69, 2], position: "after", symbol: "€" },
});
}
@ -300,97 +273,103 @@ function removeUnwantedAttrsFromTemplates(attrs) {
}
}
// alt attribute causes issues with scroll tests. Indeed, alt is
// displayed between the time we scroll programatically and the time
// we assert for the scroll position. The src attribute is removed
// as well to make sure images won't trigger a GET request on the
// server.
// Clean up templates that have already been added.
removeUnwantedAttrsFromTemplates(["alt", "src"]);
const { loadXML, getBundle, loadJS, loadCSS } = assets;
patch(assets, "TestAssetsLoadXML", {
loadXML: function (templates) {
console.log("%c[assets] fetch XML ressource", "color: #66e; font-weight: bold;");
// Clean up new templates that might be added later.
loadXML(templates);
removeUnwantedAttrsFromTemplates(["alt", "src"]);
},
getBundle: memoize(async function (xmlID) {
console.log(
"%c[assets] fetch libs from xmlID: " + xmlID,
"color: #66e; font-weight: bold;"
);
return getBundle(xmlID);
}),
loadJS: memoize(async function (ressource) {
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
function patchAssets() {
const { getBundle, loadJS, loadCSS } = assets;
patch(assets, {
getBundle: memoize(async function (xmlID) {
console.log(
"%c[assets] fetch (mock) JS ressource: " + ressource,
"%c[assets] fetch libs from xmlID: " + xmlID,
"color: #66e; font-weight: bold;"
);
return Promise.resolve();
}
console.log(
"%c[assets] fetch JS ressource: " + ressource,
"color: #66e; font-weight: bold;"
);
return loadJS(ressource);
}),
loadCSS: memoize(async function (ressource) {
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
return getBundle(xmlID);
}),
loadJS: memoize(async function (ressource) {
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
console.log(
"%c[assets] fetch (mock) JS ressource: " + ressource,
"color: #66e; font-weight: bold;"
);
return Promise.resolve();
}
console.log(
"%c[assets] fetch (mock) CSS ressource: " + ressource,
"%c[assets] fetch JS ressource: " + ressource,
"color: #66e; font-weight: bold;"
);
return Promise.resolve();
}
console.log(
"%c[assets] fetch CSS ressource: " + ressource,
"color: #66e; font-weight: bold;"
);
return loadCSS(ressource);
}),
});
return loadJS(ressource);
}),
loadCSS: memoize(async function (ressource) {
if (ressource.match(/\/static(\/\S+\/|\/)libs?/)) {
console.log(
"%c[assets] fetch (mock) CSS ressource: " + ressource,
"color: #66e; font-weight: bold;"
);
return Promise.resolve();
}
console.log(
"%c[assets] fetch CSS ressource: " + ressource,
"color: #66e; font-weight: bold;"
);
return loadCSS(ressource);
}),
});
}
function patchEventBus() {
patchWithCleanup(EventBus.prototype, {
addEventListener() {
super.addEventListener(...arguments);
registerCleanup(() => this.removeEventListener(...arguments));
},
});
}
export async function setupTests() {
// uncomment to debug memory leaks in qunit suite
// let memoryBeforeModule;
// QUnit.moduleStart(({ tests }) => {
// if (tests.length) {
// window.gc();
// memoryBeforeModule = window.performance.memory.usedJSHeapSize;
// }
// });
// QUnit.moduleDone(({ name }) => {
// if (memoryBeforeModule) {
// window.gc();
// const afterGc = window.performance.memory.usedJSHeapSize;
// console.log(
// `MEMINFO - After suite "${name}" - after gc: ${afterGc} delta: ${
// afterGc - memoryBeforeModule
// }`
// );
// memoryBeforeModule = null;
// }
// });
if (window.gc) {
// uncomment to debug memory leaks in qunit suite
// let memoryBeforeModule;
QUnit.moduleStart(({ tests }) => {
if (tests.length) {
window.gc();
// memoryBeforeModule = window.performance.memory.usedJSHeapSize;
}
});
// QUnit.moduleDone(({ name }) => {
// if (memoryBeforeModule) {
// window.gc();
// const afterGc = window.performance.memory.usedJSHeapSize;
// console.log(
// `MEMINFO - After suite "${name}" - after gc: ${afterGc} delta: ${
// afterGc - memoryBeforeModule
// }`
// );
// memoryBeforeModule = null;
// }
// });
}
QUnit.testStart(() => {
checkGlobalObjectsIntegrity();
prepareRegistriesWithCleanup();
prepareLegacyRegistriesWithCleanup();
forceLocaleAndTimezoneWithCleanup();
cleanLoadedLanguages();
patchBrowserWithCleanup();
patchCookie();
patchBodyAddEventListener();
patchLegacyBus();
patchEventBus();
patchOdoo();
patchSessionInfo();
patchOwlApp();
});
await Promise.all([whenReady(), legacyProm]);
await whenReady();
// alt attribute causes issues with scroll tests. Indeed, alt is
// displayed between the time we scroll programatically and the time
// we assert for the scroll position. The src attribute is removed
// as well to make sure images won't trigger a GET request on the
// server.
// Clean up templates that have already been added.
removeUnwantedAttrsFromTemplates(["alt", "src"]);
patchAssets();
// make sure images do not trigger a GET on the server
new MutationObserver((mutations) => {