Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,242 @@
odoo.define('web.data_manager_tests', function (require) {
"use strict";
const config = require('web.config');
const DataManager = require('web.DataManager');
const MockServer = require('web.MockServer');
const rpc = require('web.rpc');
const testUtils = require('web.test_utils');
/**
* Create a simple data manager with mocked functions:
* - mockRPC -> rpc.query
* - isDebug -> config.isDebug
* @param {Object} params
* @param {Object} params.archs
* @param {Object} params.data
* @param {Function} params.isDebug
* @param {Function} params.mockRPC
* @returns {DataManager}
*/
function createDataManager({ archs, data, isDebug, mockRPC }) {
const dataManager = new DataManager();
const server = new MockServer(data, { archs });
const serverMethods = {
async get_views({ kwargs, model }) {
const models = {};
models[model] = server.fieldsGet(model);
const views = {};
for (const [viewId, viewType] of kwargs.views) {
const arch = archs[[model, viewId || false, viewType].join()];
views[viewType] = server.getView({ arch, model, viewId });
for (const modelName of views[viewType].models) {
models[modelName] = server.fieldsGet(modelName);
}
}
const result = { models, views };
if (kwargs.options.load_filters && views.search) {
views.search.filters = data['ir.filters'].records.filter(r => r.model_id === model);
}
return result;
},
async get_filters({ args, model }) {
return data[model].records.filter(r => r.model_id === args[0]);
},
async create_or_replace({ args }) {
const id = data['ir.filters'].records.reduce((i, r) => Math.max(i, r.id), 0) + 1;
const filter = Object.assign(args[0], { id });
data['ir.filters'].records.push(filter);
return id;
},
async unlink({ args }) {
data['ir.filters'].records = data['ir.filters'].records.filter(
r => r.id !== args[0]
);
return true;
},
};
testUtils.mock.patch(rpc, {
async query({ method }) {
this._super = serverMethods[method].bind(this, ...arguments);
return mockRPC.apply(this, arguments);
},
});
testUtils.mock.patch(config, { isDebug });
return dataManager;
}
QUnit.module("Services", {
beforeEach() {
this.archs = {
'oui,10,kanban': '<kanban/>',
'oui,20,search': '<search/>',
};
this.data = {
oui: { fields: {}, records: [] },
'ir.filters': {
fields: {
context: { type: "Text", string: "Context" },
domain: { type: "Text", string: "Domain" },
model_id: { type: "Selection", string: "Model" },
name: { type: "Char", string: "Name" },
},
records: [{
id: 2,
context: '{}',
domain: '[]',
model_id: 'oui',
name: "Favorite",
}]
}
};
this.loadViewsParams = {
model: "oui",
context: {},
views_descr: [
[10, 'kanban'],
[20, 'search'],
],
};
},
afterEach() {
testUtils.mock.unpatch(rpc);
testUtils.mock.unpatch(config);
},
}, function () {
QUnit.module("Data manager");
QUnit.test("Load views with filters (non-debug mode)", async function (assert) {
assert.expect(4);
const dataManager = createDataManager({
archs: this.archs,
data: this.data,
isDebug() {
return false;
},
async mockRPC({ method, model }) {
assert.step([model, method].join('.'));
return this._super(...arguments);
},
});
const firstLoad = await dataManager.load_views(this.loadViewsParams, {
load_filters: true,
});
const secondLoad = await dataManager.load_views(this.loadViewsParams, {
load_filters: true,
});
const filters = await dataManager.load_filters({ modelName: 'oui' });
assert.deepEqual(firstLoad, secondLoad,
"query with same params and options should yield the same results");
assert.deepEqual(firstLoad.search.favoriteFilters, filters,
"load filters should yield the same result as the first load_views' filters");
assert.verifySteps(['oui.get_views'],
"only load once when not in assets debugging");
});
QUnit.test("Load views with filters (debug mode)", async function (assert) {
assert.expect(6);
const dataManager = createDataManager({
archs: this.archs,
data: this.data,
isDebug() {
return true; // assets
},
async mockRPC({ method, model }) {
assert.step([model, method].join('.'));
return this._super(...arguments);
},
});
const firstLoad = await dataManager.load_views(this.loadViewsParams, {
load_filters: true,
});
const secondLoad = await dataManager.load_views(this.loadViewsParams, {
load_filters: true,
});
const filters = await dataManager.load_filters({ modelName: 'oui' });
assert.deepEqual(firstLoad, secondLoad,
"query with same params and options should yield the same results");
assert.deepEqual(firstLoad.search.favoriteFilters, filters,
"load filters should yield the same result as the first load_views' filters");
assert.verifySteps([
'oui.get_views',
'oui.get_views',
'ir.filters.get_filters',
], "reload each time when in assets debugging");
});
QUnit.test("Cache invalidation and filters addition/deletion", async function (assert) {
assert.expect(10);
const dataManager = createDataManager({
archs: this.archs,
data: this.data,
isDebug() {
return false; // Cache only works if 'debug !== assets'
},
async mockRPC({ method, model }) {
assert.step([model, method].join('.'));
return this._super(...arguments);
},
});
// A few unnecessary 'load_filters' are done in this test to assert
// that the cache invalidation mechanics are working.
let filters;
const firstLoad = await dataManager.load_views(this.loadViewsParams, {
load_filters: true,
});
// Cache is valid -> should not trigger an RPC
filters = await dataManager.load_filters({ modelName: 'oui' });
assert.deepEqual(firstLoad.search.favoriteFilters, filters,
"load_filters and load_views.search should return the same filters");
const filterId = await dataManager.create_filter({
context: "{}",
domain: "[]",
model_id: 'oui',
name: "Temp",
});
// Cache is not valid anymore -> triggers a 'get_filters'
filters = await dataManager.load_filters({ modelName: 'oui' });
// Cache is valid -> should not trigger an RPC
filters = await dataManager.load_filters({ modelName: 'oui' });
assert.strictEqual(filters.length, 2,
"A new filter should have been added");
assert.ok(filters.find(f => f.id === filterId) === filters[filters.length - 1],
"Create filter should return the id of the last created filter");
await dataManager.delete_filter(filterId);
// Views cache is valid but filters cache is not -> triggers a 'get_filters'
const secondLoad = await dataManager.load_views(this.loadViewsParams, {
load_filters: true,
});
filters = secondLoad.search.favoriteFilters;
// Filters cache is once again valid -> no RPC
const expectedFilters = await dataManager.load_filters({ modelName: 'oui' });
assert.deepEqual(filters, expectedFilters,
"Filters loaded by the load_views should be equal to the result of a load_filters");
assert.verifySteps([
'oui.get_views',
'ir.filters.create_or_replace',
'ir.filters.get_filters',
'ir.filters.unlink',
'ir.filters.get_filters',
], "server should have been called only when needed");
});
});
});

View file

@ -0,0 +1,80 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import AbstractAction from "web.AbstractAction";
import core from "web.core";
import * as LegacyRegistry from "web.Registry";
import { registerCleanup } from "../../helpers/cleanup";
import { nextTick, patchWithCleanup } from "../../helpers/utils";
import { createWebClient, doAction } from "../../webclient/helpers";
let legacyParams;
QUnit.module("Service Provider Adapter Notification", (hooks) => {
hooks.beforeEach(() => {
legacyParams = {
serviceRegistry: new LegacyRegistry(),
};
});
QUnit.test(
"can display and close a sticky danger notification with a title (legacy)",
async function (assert) {
assert.expect(8);
let notifId;
let timeoutCB;
patchWithCleanup(browser, {
setTimeout: (cb, delay) => {
if (!delay) {
return; // Timeouts from router service
}
timeoutCB = cb;
assert.step("time: " + delay);
return 1;
},
});
const NotifyAction = AbstractAction.extend({
on_attach_callback() {
notifId = this.call("notification", "notify", {
title: "Some title",
message: "I'm a danger notification",
type: "danger",
sticky: true,
});
},
});
const CloseAction = AbstractAction.extend({
on_attach_callback() {
this.call("notification", "close", notifId, false, 3000);
},
});
core.action_registry.add("NotifyTestLeg", NotifyAction);
core.action_registry.add("CloseTestLeg", CloseAction);
registerCleanup(() => {
delete core.action_registry.map.NotifyTestLeg;
delete core.action_registry.map.CloseTestLeg;
});
const webClient = await createWebClient({ legacyParams });
await doAction(webClient, "NotifyTestLeg");
await nextTick();
assert.containsOnce(document.body, ".o_notification");
const notif = document.body.querySelector(".o_notification");
assert.strictEqual(notif.querySelector(".o_notification_title").textContent, "Some title");
assert.strictEqual(
notif.querySelector(".o_notification_content").textContent,
"I'm a danger notification"
);
assert.hasClass(notif, "border-danger");
//Close the notification
await doAction(webClient, "CloseTestLeg");
await nextTick();
assert.containsOnce(document.body, ".o_notification");
// simulate end of timeout
timeoutCB();
await nextTick();
assert.containsNone(document.body, ".o_notification");
assert.verifySteps(["time: 3000"]);
}
);
});