Files
odoo_source/addons/web/static/tests/model/sample_server_tests.js
T
8723f020c3 [REF] web: new RelationalModel
Before this commit, we had several implementations of a relational
model. The form view used the BasicRelationalModel, which was a
wrapper around the BasicModel. The list view used the
RelationalModel (a simpler version that didn't support complex
usecases of x2many fields). The kanban view used the KanbanModel,
an extension of RelationalModel.

This commit removes all these, and replaces them by a single model.
It is called RelationalModel, and as a similar structure as its
predecessor (with datapoints). It now handles tricky x2many usecases
of the form view though.

This commit also comes with several important changes:
 1) The new model is based on unity reads [1]. This means that all
 data required by the view is fetched at once, and datapoints are
 created with their data (whereas before, they were responsible to
 fetch their data). There's an exception for special data: they are
 no longer handler by the model. A hook "useSpecialData" has been
 created, and it allows fields (components) to fetch additional
 data they require.

 2) The new model uses onchange2 [2]. With onchange2, we send and
 receive a diff, instead of the whole state of the record. Morever,
 onchange2 uses unity, which means that returned commands for x2manys
 (e.g. command 4, LINK) contain the values of the corresponding
 records, so we don't need to fetch them in a separate call
 afterwards.

 3) The new model no longer triggers deep renderings (render(true)).
 It is instead based on fine-grained reactivity (i.e. only what
 depends on what has changed is re-rendered). We introduced a hook
 "observeRecord" which allows a component to subscribe itself to
 some keys in a record datapoint, and register a callback to
 execute when those keys change (e.g. to fetch data).

[1] odoo/odoo@7d2baaa0c7
[2] odoo/odoo@f5e6494da3

Task~3179751

Part-of: odoo/odoo#114024
Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: FrancoisGe <fge@odoo.com>
Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
Co-authored-by: Pierre Rousseau <pro@odoo.com>
2023-07-24 20:17:50 +02:00

535 lines
19 KiB
JavaScript

/** @odoo-module **/
import { SampleServer } from "@web/model/sample_server";
const {
MAIN_RECORDSET_SIZE,
SEARCH_READ_LIMIT, // Limits
SAMPLE_COUNTRIES,
SAMPLE_PEOPLE,
SAMPLE_TEXTS, // Text values
MAX_COLOR_INT,
MAX_FLOAT,
MAX_INTEGER,
MAX_MONETARY, // Number values
SUB_RECORDSET_SIZE, // Records sise
} = SampleServer;
/**
* Transforms random results into deterministic ones.
*/
class DeterministicSampleServer extends SampleServer {
constructor() {
super(...arguments);
this.arrayElCpt = 0;
this.boolCpt = 0;
this.subRecordIdCpt = 0;
}
_getRandomArrayEl(array) {
return array[this.arrayElCpt++ % array.length];
}
_getRandomBool() {
return Boolean(this.boolCpt++ % 2);
}
_getRandomSubRecordId() {
return (this.subRecordIdCpt++ % SUB_RECORDSET_SIZE) + 1;
}
}
let fields;
QUnit.module(
"Sample Server",
{
beforeEach() {
fields = {
"res.users": {
display_name: { string: "Name", type: "char" },
name: { string: "Reference", type: "char" },
email: { string: "Email", type: "char" },
phone_number: { string: "Phone number", type: "char" },
brol_machin_url_truc: { string: "URL", type: "char" },
urlemailphone: { string: "Whatever", type: "char" },
active: { string: "Active", type: "boolean" },
is_alive: { string: "Is alive", type: "boolean" },
description: { string: "Description", type: "text" },
birthday: { string: "Birthday", type: "date" },
arrival_date: { string: "Date of arrival", type: "datetime" },
height: { string: "Height", type: "float" },
color: { string: "Color", type: "integer" },
age: { string: "Age", type: "integer" },
salary: { string: "Salary", type: "monetary" },
currency: {
string: "Currency",
type: "many2one",
relation: "res.currency",
},
manager_id: {
string: "Manager",
type: "many2one",
relation: "res.users",
},
cover_image_id: {
string: "Cover Image",
type: "many2one",
relation: "ir.attachment",
},
managed_ids: {
string: "Managing",
type: "one2many",
relation: "res.users",
},
tag_ids: { string: "Tags", type: "many2many", relation: "tag" },
type: {
string: "Type",
type: "selection",
selection: [
["client", "Client"],
["partner", "Partner"],
["employee", "Employee"],
],
},
},
"res.country": {
display_name: { string: "Name", type: "char" },
},
hobbit: {
display_name: { string: "Name", type: "char" },
profession: {
string: "Profession",
type: "selection",
selection: [
["gardener", "Gardener"],
["brewer", "Brewer"],
["adventurer", "Adventurer"],
],
},
age: { string: "Age", type: "integer" },
},
"ir.attachment": {
display_name: { string: "Name", type: "char" },
},
};
},
},
function () {
QUnit.module("Basic behaviour");
QUnit.test("Sample data: people type + all field names", async function (assert) {
assert.expect(26);
const allFieldNames = Object.keys(fields["res.users"]);
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
const { records } = await server.mockRpc({
method: "web_search_read",
model: "res.users",
fields: allFieldNames,
});
const rec = records[0];
function assertFormat(fieldName, regex) {
if (regex instanceof RegExp) {
assert.ok(
regex.test(rec[fieldName].toString()),
`Field "${fieldName}" has the correct format`
);
} else {
assert.strictEqual(
typeof rec[fieldName],
regex,
`Field "${fieldName}" is of type ${regex}`
);
}
}
function assertBetween(fieldName, min, max) {
const val = rec[fieldName];
assert.ok(
min <= val && val < max && parseInt(val, 10) === val,
`Field "${fieldName}" should be an integer between ${min} and ${max}: ${val}`
);
}
// Basic fields
assert.ok(SAMPLE_PEOPLE.includes(rec.display_name));
assert.ok(SAMPLE_PEOPLE.includes(rec.name));
assert.strictEqual(
rec.email,
`${rec.display_name.replace(/ /, ".").toLowerCase()}@sample.demo`
);
assertFormat("phone_number", /\+1 555 754 000\d/);
assertFormat("brol_machin_url_truc", /http:\/\/sample\d\.com/);
assert.strictEqual(rec.urlemailphone, false);
assert.strictEqual(rec.active, true);
assertFormat("is_alive", "boolean");
assert.ok(SAMPLE_TEXTS.includes(rec.description));
assertFormat("birthday", /\d{4}-\d{2}-\d{2}/);
assertFormat("arrival_date", /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
assert.ok(
rec.height >= 0 && rec.height <= MAX_FLOAT,
"Field height should be between 0 and 100"
);
assertBetween("color", 0, MAX_COLOR_INT);
assertBetween("age", 0, MAX_INTEGER);
assertBetween("salary", 0, MAX_MONETARY);
// check float field have 2 decimal rounding
assert.strictEqual(rec.height, parseFloat(parseFloat(rec.height).toFixed(2)));
const selectionValues = fields["res.users"].type.selection.map((sel) => sel[0]);
assert.ok(selectionValues.includes(rec.type));
// Relational fields
assert.strictEqual(rec.currency[0], 1);
// Currently we expect the currency name to be a latin string, which
// is not important; in most case we only need the ID. The following
// assertion can be removed if needed.
assert.ok(SAMPLE_TEXTS.includes(rec.currency[1]));
assert.strictEqual(typeof rec.manager_id[0], "number");
assert.ok(SAMPLE_PEOPLE.includes(rec.manager_id[1]));
assert.strictEqual(rec.cover_image_id, false);
assert.strictEqual(rec.managed_ids.length, 2);
assert.ok(rec.managed_ids.every((id) => typeof id === "number"));
assert.strictEqual(rec.tag_ids.length, 2);
assert.ok(rec.tag_ids.every((id) => typeof id === "number"));
});
QUnit.test("Sample data: country type", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer("res.country", fields["res.country"]);
const { records } = await server.mockRpc({
method: "web_search_read",
model: "res.country",
fields: ["display_name"],
});
assert.ok(SAMPLE_COUNTRIES.includes(records[0].display_name));
});
QUnit.test("Sample data: any type", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const { records } = await server.mockRpc({
method: "web_search_read",
model: "hobbit",
fields: ["display_name"],
});
assert.ok(SAMPLE_TEXTS.includes(records[0].display_name));
});
QUnit.module("RPC calls");
QUnit.test("Send 'search_read' RPC: valid field names", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "web_search_read",
model: "hobbit",
fields: ["display_name"],
});
assert.deepEqual(Object.keys(result.records[0]), ["id", "display_name"]);
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
assert.ok(/\w+/.test(result.records[0].display_name), "Display name has been mocked");
});
QUnit.test("Send 'search_read' RPC: invalid field names", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "web_search_read",
model: "hobbit",
fields: ["name"],
});
assert.deepEqual(Object.keys(result.records[0]), ["id", "name"]);
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
assert.strictEqual(
result.records[0].name,
false,
`Field "name" doesn't exist => returns false`
);
});
QUnit.test("Send 'web_read_group' RPC: no group", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
server.setExistingGroups(null);
const result = await server.mockRpc({
method: "web_read_group",
model: "hobbit",
groupBy: ["profession"],
});
assert.deepEqual(result, {
groups: [
{
__domain: [],
profession: "adventurer",
profession_count: 5,
},
{
__domain: [],
profession: "brewer",
profession_count: 5,
},
{
__domain: [],
profession: "gardener",
profession_count: 6,
},
],
length: 3,
});
});
QUnit.test("Send 'web_read_group' RPC: 2 groups", async function (assert) {
assert.expect(5);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const existingGroups = [
{ profession: "gardener", count: 0 }, // fake group
{ profession: "adventurer", count: 0 }, // fake group
];
server.setExistingGroups(existingGroups);
const result = await server.mockRpc({
method: "web_read_group",
model: "hobbit",
groupBy: ["profession"],
fields: [],
});
assert.strictEqual(result.length, 2);
assert.strictEqual(result.groups.length, 2);
assert.deepEqual(
result.groups.map((g) => g.profession),
["gardener", "adventurer"]
);
assert.strictEqual(
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE
);
assert.ok(result.groups.every((g) => g.profession_count === g.__data.length));
});
QUnit.test("Send 'web_read_group' RPC: all groups", async function (assert) {
assert.expect(5);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const existingGroups = [
{ profession: "gardener", count: 0 }, // fake group
{ profession: "brewer", count: 0 }, // fake group
{ profession: "adventurer", count: 0 }, // fake group
];
server.setExistingGroups(existingGroups);
const result = await server.mockRpc({
method: "web_read_group",
model: "hobbit",
groupBy: ["profession"],
fields: [],
});
assert.strictEqual(result.length, 3);
assert.strictEqual(result.groups.length, 3);
assert.deepEqual(
result.groups.map((g) => g.profession),
["gardener", "brewer", "adventurer"]
);
assert.strictEqual(
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE
);
assert.ok(result.groups.every((g) => g.profession_count === g.__data.length));
});
QUnit.test("Send 'read_group' RPC: no group", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "read_group",
model: "hobbit",
fields: [],
groupBy: [],
});
assert.deepEqual(result, [
{
__count: MAIN_RECORDSET_SIZE,
__domain: [],
},
]);
});
QUnit.test("Send 'read_group' RPC: groupBy", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "read_group",
model: "hobbit",
fields: [],
groupBy: ["profession"],
});
assert.strictEqual(result.length, 3);
assert.deepEqual(
result.map((g) => g.profession),
["adventurer", "brewer", "gardener"]
);
assert.strictEqual(
result.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE
);
});
QUnit.test("Send 'read_group' RPC: groupBy and field", async function (assert) {
assert.expect(4);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "read_group",
model: "hobbit",
fields: ["age:sum"],
groupBy: ["profession"],
});
assert.strictEqual(result.length, 3);
assert.deepEqual(
result.map((g) => g.profession),
["adventurer", "brewer", "gardener"]
);
assert.strictEqual(
result.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE
);
assert.strictEqual(
result.reduce((acc, g) => acc + g.age, 0),
server.data.hobbit.records.reduce((acc, g) => acc + g.age, 0)
);
});
QUnit.test("Send 'read_group' RPC: multiple groupBys and lazy", async function (assert) {
assert.expect(2);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "read_group",
model: "hobbit",
fields: [],
groupBy: ["profession", "age"],
});
assert.ok("profession" in result[0]);
assert.notOk("age" in result[0]);
});
QUnit.test(
"Send 'read_group' RPC: multiple groupBys and not lazy",
async function (assert) {
assert.expect(2);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "read_group",
model: "hobbit",
fields: [],
groupBy: ["profession", "age"],
lazy: false,
});
assert.ok("profession" in result[0]);
assert.ok("age" in result[0]);
}
);
QUnit.test(
"Send 'read_group' RPC: multiple groupBys among which a many2many",
async function (assert) {
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
const result = await server.mockRpc({
method: "read_group",
model: "res.users",
fields: [],
groupBy: ["height", "tag_ids"],
lazy: false,
});
assert.ok(typeof result[0].tag_ids[0] === "number");
assert.ok(typeof result[0].tag_ids[1] === "string");
}
);
QUnit.test("Send 'read' RPC: no id", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "read",
model: "hobbit",
args: [[], ["display_name"]],
});
assert.deepEqual(result, []);
});
QUnit.test("Send 'read' RPC: one id", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const result = await server.mockRpc({
method: "read",
model: "hobbit",
args: [[1], ["display_name"]],
});
assert.strictEqual(result.length, 1);
assert.ok(/\w+/.test(result[0].display_name), "Display name has been mocked");
assert.strictEqual(result[0].id, 1);
});
QUnit.test("Send 'read' RPC: more than all available ids", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
const amount = MAIN_RECORDSET_SIZE + 3;
const ids = new Array(amount).fill().map((_, i) => i + 1);
const result = await server.mockRpc({
method: "read",
model: "hobbit",
args: [ids, ["display_name"]],
});
assert.strictEqual(result.length, MAIN_RECORDSET_SIZE);
});
// To be implemented if needed
// QUnit.test("Send 'read_progress_bar' RPC", async function (assert) { ... });
}
);