mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 03:52:01 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
826
odoo-bringout-oca-ocb-web/web/static/tests/model/record.test.js
Normal file
826
odoo-bringout-oca-ocb-web/web/static/tests/model/record.test.js
Normal file
|
|
@ -0,0 +1,826 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, onError, useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
findComponent,
|
||||
makeServerError,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { Record } from "@web/model/record";
|
||||
import { RelationalModel } from "@web/model/relational_model/relational_model";
|
||||
import { useRecordObserver } from "@web/model/relational_model/utils";
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
class Foo extends models.Model {
|
||||
foo = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, foo: "yop" },
|
||||
{ id: 2, foo: "blip" },
|
||||
{ id: 3, foo: "gnap" },
|
||||
{ id: 4, foo: "abc" },
|
||||
{ id: 5, foo: "blop" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Foo]);
|
||||
|
||||
test(`display a simple field`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<div class="root">
|
||||
<Record resModel="'foo'" resId="1" fieldNames="['foo']" t-slot-scope="data">
|
||||
<span>hello</span>
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect(queryFirst`.root`).toHaveOuterHTML(`
|
||||
<div class="root">
|
||||
<span>hello</span>
|
||||
<div name="foo" class="o_field_widget o_field_char">
|
||||
<span>yop</span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
expect.verifySteps([
|
||||
"/web/dataset/call_kw/foo/fields_get",
|
||||
"/web/dataset/call_kw/foo/web_read",
|
||||
]);
|
||||
});
|
||||
|
||||
test(`can be updated with different resId`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" resId="state.resId" fieldNames="['foo']" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
<button class="my-btn" t-on-click="() => this.state.resId++">Next</button>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
resId: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps([
|
||||
"/web/dataset/call_kw/foo/fields_get",
|
||||
"/web/dataset/call_kw/foo/web_read",
|
||||
]);
|
||||
expect(`.o_field_char:contains(yop)`).toHaveCount(1);
|
||||
|
||||
await contains(`button.my-btn`).click();
|
||||
expect(`.o_field_char:contains(blip)`).toHaveCount(1);
|
||||
expect.verifySteps(["/web/dataset/call_kw/foo/web_read"]);
|
||||
});
|
||||
|
||||
test(`can be receive a context as props`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<div class="root">
|
||||
<Record resModel="'foo'" fieldNames="['foo']" context="{ test: 4 }" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
onRpc("onchange", ({ kwargs }) => {
|
||||
expect.step(`onchange`);
|
||||
expect(kwargs.context).toEqual({
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
test: 4,
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
});
|
||||
});
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
|
||||
test(`predefined fields and values`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "char",
|
||||
},
|
||||
bar: {
|
||||
name: "bar",
|
||||
type: "boolean",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: "abc",
|
||||
bar: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps([]);
|
||||
expect(`.o_field_widget input`).toHaveValue("abc");
|
||||
});
|
||||
|
||||
test(`Record with onRootLoaded props`, async () => {
|
||||
let record;
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" t-slot-scope="data" hooks="hooks">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "char",
|
||||
},
|
||||
};
|
||||
this.hooks = {
|
||||
onRootLoaded: this.onRootLoaded.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
onRootLoaded(root) {
|
||||
expect.step("onRootLoaded");
|
||||
record = root;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["onRootLoaded"]);
|
||||
expect(record.data.foo).toBe("");
|
||||
await contains(`[name='foo'] input`).edit("coucou");
|
||||
expect(record.data.foo).toBe("coucou");
|
||||
});
|
||||
|
||||
test(`Record with onRecordChanged props`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data" hooks="hooks">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "char",
|
||||
},
|
||||
bar: {
|
||||
name: "bar",
|
||||
type: "boolean",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: "abc",
|
||||
bar: true,
|
||||
};
|
||||
this.hooks = {
|
||||
onRecordChanged: this.onRecordChanged.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
onRecordChanged(record, changes) {
|
||||
expect.step("record changed");
|
||||
expect(record.model.constructor.name).toBe("StandaloneRelationalModel");
|
||||
expect(changes).toEqual({ foo: "753" });
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect(`[name='foo'] input`).toHaveValue("abc");
|
||||
|
||||
await contains(`[name='foo'] input`).edit("753");
|
||||
expect.verifySteps(["record changed"]);
|
||||
expect(`[name='foo'] input`).toHaveValue("753");
|
||||
});
|
||||
|
||||
test(`Record with onWillSaveRecord and onRecordSavedProps`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" resId="1" fieldNames="['foo']" mode="'edit'" t-slot-scope="data" hooks="hooks">
|
||||
<button class="save" t-on-click="() => data.record.save()">Save</button>
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.hooks = {
|
||||
onRecordSaved: this.onRecordSaved.bind(this),
|
||||
onWillSaveRecord: this.onWillSaveRecord.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
onRecordSaved(record) {
|
||||
expect.step("onRecordSaved");
|
||||
}
|
||||
|
||||
onWillSaveRecord(record) {
|
||||
expect.step("onWillSaveRecord");
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ method }) => expect.step(method));
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
await contains(`[name='foo'] input`).edit("abc");
|
||||
await contains(`button.save`).click();
|
||||
expect.verifySteps(["fields_get", "web_read", "onWillSaveRecord", "web_save", "onRecordSaved"]);
|
||||
});
|
||||
|
||||
test(`can access record changes`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" t-slot-scope="data">
|
||||
<button class="do_something" t-on-click="() => doSomething(data.record)">
|
||||
Do something
|
||||
</button>
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
async doSomething(record) {
|
||||
expect.step(`do something with ${JSON.stringify(await record.getChanges())}`);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
await contains(".do_something").click();
|
||||
expect.verifySteps([`do something with {"foo":false}`]);
|
||||
|
||||
await contains(".o_field_widget[name=foo] input").edit("some value");
|
||||
await contains(".do_something").click();
|
||||
expect.verifySteps([`do something with {"foo":"some value"}`]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`handles many2one fields: value is an object`, async () => {
|
||||
class Bar extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "bar1" },
|
||||
{ id: 3, name: "abc" },
|
||||
];
|
||||
}
|
||||
defineModels([Bar]);
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Many2OneField };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data" hooks="hooks">
|
||||
<Many2OneField name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "many2one",
|
||||
relation: "bar",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: { id: 1, display_name: "bar1" },
|
||||
};
|
||||
this.hooks = {
|
||||
onRecordChanged: this.onRecordChanged.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
onRecordChanged(record, changes) {
|
||||
expect.step("record changed");
|
||||
expect(changes).toEqual({ foo: 3 });
|
||||
expect(record.data).toEqual({ foo: { id: 3, display_name: "abc" } });
|
||||
expect(record.data.foo.id).toBe(3);
|
||||
expect(record.data.foo.display_name).toBe("abc");
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps([]);
|
||||
expect(`.o_field_many2one_selection input`).toHaveValue("bar1");
|
||||
|
||||
await contains(`.o_field_many2one_selection input`).edit("abc", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["/web/dataset/call_kw/bar/web_name_search"]);
|
||||
|
||||
await contains(`.o-autocomplete--dropdown-item a:eq(0)`).click();
|
||||
expect.verifySteps(["record changed"]);
|
||||
expect(`.o_field_many2one_selection input`).toHaveValue("abc");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test(`handles many2one fields: value is a pair id, display_name`, async () => {
|
||||
class Bar extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "bar1" },
|
||||
{ id: 3, name: "abc" },
|
||||
];
|
||||
}
|
||||
defineModels([Bar]);
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Many2OneField };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data" hooks="hooks">
|
||||
<Many2OneField name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "many2one",
|
||||
relation: "bar",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: { id: 1, display_name: "bar1" },
|
||||
};
|
||||
this.hooks = {
|
||||
onRecordChanged: this.onRecordChanged.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
onRecordChanged(record, changes) {
|
||||
expect.step("record changed");
|
||||
expect(changes).toEqual({ foo: 3 });
|
||||
expect(record.data).toEqual({ foo: { id: 3, display_name: "abc" } });
|
||||
expect(record.data.foo.id).toBe(3);
|
||||
expect(record.data.foo.display_name).toBe("abc");
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps([]);
|
||||
expect(`.o_field_many2one_selection input`).toHaveValue("bar1");
|
||||
|
||||
await contains(`.o_field_many2one_selection input`).edit("abc", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["/web/dataset/call_kw/bar/web_name_search"]);
|
||||
|
||||
await contains(`.o-autocomplete--dropdown-item a:eq(0)`).click();
|
||||
expect.verifySteps(["record changed"]);
|
||||
expect(`.o_field_many2one_selection input`).toHaveValue("abc");
|
||||
});
|
||||
|
||||
test(`handles many2one fields: value is an id`, async () => {
|
||||
class Bar extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "bar1" },
|
||||
{ id: 3, name: "abc" },
|
||||
];
|
||||
}
|
||||
defineModels([Bar]);
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Many2OneField };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
|
||||
<Many2OneField name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "many2one",
|
||||
relation: "bar",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["/web/dataset/call_kw/bar/web_read"]);
|
||||
expect(`.o_field_many2one_selection input`).toHaveValue("bar1");
|
||||
});
|
||||
|
||||
test(`handles many2one fields: value is an object with id only`, async () => {
|
||||
class Bar extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "bar1" },
|
||||
{ id: 3, name: "abc" },
|
||||
];
|
||||
}
|
||||
defineModels([Bar]);
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Many2OneField };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
|
||||
<Many2OneField name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "many2one",
|
||||
relation: "bar",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: { id: 1 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["/web/dataset/call_kw/bar/web_read"]);
|
||||
expect(`.o_field_many2one_selection input`).toHaveValue("bar1");
|
||||
});
|
||||
|
||||
test(`handles x2many fields`, async () => {
|
||||
class Tag extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "bug" },
|
||||
{ id: 3, name: "ref" },
|
||||
];
|
||||
}
|
||||
defineModels([Tag]);
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Many2ManyTagsField };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['tags']" activeFields="activeFields" fields="fields" values="values" t-slot-scope="data">
|
||||
<Many2ManyTagsField name="'tags'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.activeFields = {
|
||||
tags: {
|
||||
related: {
|
||||
activeFields: {
|
||||
display_name: {},
|
||||
},
|
||||
fields: {
|
||||
display_name: { name: "display_name", type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
this.fields = {
|
||||
tags: {
|
||||
name: "Tags",
|
||||
type: "many2many",
|
||||
relation: "tag",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
tags: [1, 3],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["/web/dataset/call_kw/tag/web_read"]);
|
||||
expect(queryAllTexts`.o_tag`).toEqual(["bug", "ref"]);
|
||||
});
|
||||
|
||||
test(`supports passing dynamic values -- full control to the user of Record`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<Record resModel="'foo'" fieldNames="['foo']" fields="fields" values="{ foo: values.foo }" t-slot-scope="data" hooks="hooks">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "char",
|
||||
},
|
||||
bar: {
|
||||
name: "bar",
|
||||
type: "boolean",
|
||||
},
|
||||
};
|
||||
this.values = useState({
|
||||
foo: "abc",
|
||||
bar: true,
|
||||
});
|
||||
this.hooks = {
|
||||
onRecordChanged: this.onRecordChanged.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
onRecordChanged(record, changes) {
|
||||
expect.step("record changed");
|
||||
expect(record.model.constructor.name).toBe("StandaloneRelationalModel");
|
||||
expect(changes).toEqual({ foo: "753" });
|
||||
this.values.foo = 357;
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(() => {
|
||||
throw new makeServerError({ message: "should not do any rpc" });
|
||||
});
|
||||
await mountWithCleanup(Parent);
|
||||
expect(`[name='foo'] input`).toHaveValue("abc");
|
||||
|
||||
await contains(`[name='foo'] input`).edit("753");
|
||||
expect.verifySteps(["record changed"]);
|
||||
expect(`[name='foo'] input`).toHaveValue("357");
|
||||
});
|
||||
|
||||
test(`can switch records`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<a id="increment" t-on-click="() => state.num++" t-esc="state.num"/>
|
||||
<a id="next" t-on-click="next">NEXT</a>
|
||||
<Record resId="state.currentId" resModel="'foo'" fieldNames="['foo']" fields="fields" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ currentId: 1, num: 0 });
|
||||
}
|
||||
|
||||
next() {
|
||||
this.state.currentId = 5;
|
||||
this.state.num++;
|
||||
}
|
||||
}
|
||||
|
||||
onRpc("web_read", ({ method, args, kwargs }) => {
|
||||
expect.step(
|
||||
`${method} : ${JSON.stringify(args[0])} - ${JSON.stringify(kwargs.specification)}`
|
||||
);
|
||||
});
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps([`web_read : [1] - {"foo":{}}`]);
|
||||
expect(`#increment`).toHaveText("0");
|
||||
expect(`div[name='foo']`).toHaveText("yop");
|
||||
|
||||
await contains(`#increment`).click();
|
||||
// No reload when a render from upstream comes
|
||||
expect.verifySteps([]);
|
||||
expect(`#increment`).toHaveText("1");
|
||||
expect(`div[name='foo']`).toHaveText("yop");
|
||||
|
||||
await contains(`#next`).click();
|
||||
expect.verifySteps([`web_read : [5] - {"foo":{}}`]);
|
||||
expect(`#increment`).toHaveText("2");
|
||||
expect(`div[name='foo']`).toHaveText("blop");
|
||||
});
|
||||
|
||||
test(`can switch records with values`, async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<a id="next" t-on-click="next">NEXT</a>
|
||||
<Record resId="state.currentId" resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.fields = {
|
||||
foo: {
|
||||
name: "foo",
|
||||
type: "char",
|
||||
},
|
||||
bar: {
|
||||
name: "bar",
|
||||
type: "boolean",
|
||||
},
|
||||
};
|
||||
this.values = {
|
||||
foo: "abc",
|
||||
bar: true,
|
||||
};
|
||||
this.state = useState({ currentId: 99 });
|
||||
}
|
||||
|
||||
next() {
|
||||
this.state.currentId = 100;
|
||||
this.values = {
|
||||
foo: "def",
|
||||
bar: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onRpc(({ route }) => expect.step(route));
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
const _record = findComponent(
|
||||
parent,
|
||||
(component) => component instanceof Record.components._Record
|
||||
);
|
||||
|
||||
// No load since the values are provided to the record
|
||||
expect.verifySteps([]);
|
||||
// First values are loaded
|
||||
expect(`div[name='foo']`).toHaveText("abc");
|
||||
// Verify that the underlying _Record Model root has the specified resId
|
||||
expect(_record.model.root.resId).toBe(99);
|
||||
|
||||
await contains(`#next`).click();
|
||||
// Still no load.
|
||||
expect.verifySteps([]);
|
||||
// Second values are loaded
|
||||
expect(`div[name='foo']`).toHaveText("def");
|
||||
// Verify that the underlying _Record Model root has the updated resId
|
||||
expect(_record.model.root.resId).toBe(100);
|
||||
});
|
||||
|
||||
test(`faulty useRecordObserver in widget`, async () => {
|
||||
patchWithCleanup(CharField.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
useRecordObserver((record, props) => {
|
||||
throw new Error("faulty record observer");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<t t-if="!state.error">
|
||||
<Record resId="1" resModel="'foo'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
|
||||
<Field name="'foo'" record="data.record"/>
|
||||
</Record>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="error" t-esc="state.error.message"/>
|
||||
</t>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ error: false });
|
||||
onError((error) => {
|
||||
this.state.error = error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(`.error`).toHaveText(
|
||||
`The following error occurred in onWillStart: "faulty record observer"`
|
||||
);
|
||||
});
|
||||
|
||||
test(`don't duplicate a useRecordObserver effect when switching back and forth between the same records`, async () => {
|
||||
patchWithCleanup(CharField.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
useRecordObserver((record) => {
|
||||
expect.step(`foo: ${record.data.foo}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class StandaloneRelationalModel extends RelationalModel {
|
||||
constructor(env, params, services) {
|
||||
params = {
|
||||
config: {
|
||||
resModel: "foo",
|
||||
fieldNames: ["foo"],
|
||||
fields: { foo: { name: "foo", type: "char" } },
|
||||
activeFields: { foo: {} },
|
||||
isMonoRecord: true,
|
||||
},
|
||||
hooks: {
|
||||
onRecordSaved: () => {},
|
||||
onWillSaveRecord: () => {},
|
||||
onRecordChanged: () => {},
|
||||
},
|
||||
};
|
||||
super(env, params, services);
|
||||
}
|
||||
load(params = {}) {
|
||||
const data = params.values;
|
||||
const config = this._getNextConfig(this.config, params);
|
||||
this.root = this._createRoot(config, data);
|
||||
this.config = config;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { Record, Field };
|
||||
static template = xml`
|
||||
<a id="setRecord" t-on-click="setRecord">SET</a>
|
||||
<a id="toggleRecord" t-on-click="toggleRecord">TOGGLE</a>
|
||||
<Field name="'foo'" record="records[state.recordIndex]"/>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
const services = { orm: this.orm };
|
||||
const model = new StandaloneRelationalModel(this.env, {}, services);
|
||||
model.load({ resId: 1, values: { foo: "abc" } });
|
||||
const record1 = model.root;
|
||||
model.load({ resId: 2, values: { foo: "def" } });
|
||||
const record2 = model.root;
|
||||
this.records = [record1, record2];
|
||||
this.state = useState({ recordIndex: 0 });
|
||||
}
|
||||
|
||||
setRecord() {
|
||||
this.records[this.state.recordIndex].update({ foo: "ghi" });
|
||||
}
|
||||
|
||||
toggleRecord() {
|
||||
this.state.recordIndex = (this.state.recordIndex + 1) % 2;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["foo: abc"]);
|
||||
await contains("#toggleRecord").click();
|
||||
expect.verifySteps(["foo: def"]);
|
||||
await contains("#toggleRecord").click();
|
||||
expect.verifySteps(["foo: abc"]);
|
||||
await contains("#setRecord").click();
|
||||
expect.verifySteps(["foo: ghi"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const 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" },
|
||||
website_url: { 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" },
|
||||
},
|
||||
};
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("Sample data", () => {
|
||||
test("people type + all field names", async () => {
|
||||
const specification = {};
|
||||
for (const fieldName in fields["res.users"]) {
|
||||
specification[fieldName] = {};
|
||||
if (fields["res.users"][fieldName].type === "many2one") {
|
||||
specification[fieldName] = {
|
||||
fields: { display_name: {} },
|
||||
};
|
||||
}
|
||||
}
|
||||
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
|
||||
const { records } = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "res.users",
|
||||
specification,
|
||||
});
|
||||
const rec = records[0];
|
||||
// Basic fields
|
||||
expect(SAMPLE_PEOPLE).toInclude(rec.display_name);
|
||||
expect(SAMPLE_PEOPLE).toInclude(rec.name);
|
||||
expect(rec.email).toBe(`${rec.display_name.replace(/ /, ".").toLowerCase()}@sample.demo`);
|
||||
expect(rec.phone_number).toMatch(/\+1 555 754 000\d/);
|
||||
expect(rec.website_url).toMatch(/http:\/\/sample\d\.com/);
|
||||
expect(rec.urlemailphone).toBe(false);
|
||||
expect(rec.active).toBe(true);
|
||||
expect(rec.is_alive).toBeOfType("boolean");
|
||||
expect(SAMPLE_TEXTS).toInclude(rec.description);
|
||||
expect(rec.birthday).toMatch(/\d{4}-\d{2}-\d{2}/);
|
||||
expect(rec.arrival_date).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
||||
expect(rec.height).toBeWithin(0, MAX_FLOAT);
|
||||
expect(rec.color).toBeWithin(0, MAX_COLOR_INT - 1);
|
||||
expect(rec.age).toBeWithin(0, MAX_INTEGER - 1);
|
||||
expect(rec.salary).toBeWithin(0, MAX_MONETARY - 1);
|
||||
// check float field have 2 decimal rounding
|
||||
expect(rec.height).toBe(parseFloat(parseFloat(rec.height).toFixed(2)));
|
||||
const selectionValues = fields["res.users"].type.selection.map((sel) => sel[0]);
|
||||
expect(selectionValues).toInclude(rec.type);
|
||||
// Relational fields
|
||||
expect(rec.currency.id).toBe(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.
|
||||
expect(SAMPLE_TEXTS).toInclude(rec.currency.display_name);
|
||||
expect(rec.manager_id.id).toBeOfType("number");
|
||||
expect(SAMPLE_PEOPLE).toInclude(rec.manager_id.display_name);
|
||||
expect(rec.cover_image_id).toBe(false);
|
||||
expect(rec.managed_ids).toHaveLength(2);
|
||||
expect(rec.managed_ids.every((id) => typeof id === "number")).toBe(true);
|
||||
expect(rec.tag_ids).toHaveLength(2);
|
||||
expect(rec.tag_ids.every((id) => typeof id === "number")).toBe(true);
|
||||
});
|
||||
|
||||
test("country type", async () => {
|
||||
const server = new DeterministicSampleServer("res.country", fields["res.country"]);
|
||||
const { records } = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "res.country",
|
||||
specification: { display_name: {} },
|
||||
});
|
||||
expect(SAMPLE_COUNTRIES).toInclude(records[0].display_name);
|
||||
});
|
||||
|
||||
test("any type", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const { records } = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "hobbit",
|
||||
specification: { display_name: {} },
|
||||
});
|
||||
expect(SAMPLE_TEXTS).toInclude(records[0].display_name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RPC calls", () => {
|
||||
test("'search_read': valid field names", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const result = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "hobbit",
|
||||
specification: { display_name: {} },
|
||||
});
|
||||
expect(Object.keys(result.records[0])).toEqual(["id", "display_name"]);
|
||||
expect(result.length).toBe(SEARCH_READ_LIMIT);
|
||||
expect(result.records[0].display_name).toMatch(/\w+/, {
|
||||
message: "Display name has been mocked",
|
||||
});
|
||||
});
|
||||
|
||||
test("'search_read': many2one fields", async () => {
|
||||
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
|
||||
const result = await server.mockRpc({
|
||||
method: "web_search_read",
|
||||
model: "res.users",
|
||||
specification: {
|
||||
manager_id: {
|
||||
fields: { display_name: {} },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(Object.keys(result.records[0])).toEqual(["id", "manager_id"]);
|
||||
expect(result.records[0].manager_id.id).toBe(1);
|
||||
expect(result.records[0].manager_id.display_name).toMatch(/\w+/);
|
||||
});
|
||||
|
||||
test("'web_read_group': no group", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
server.setExistingGroups(null);
|
||||
const result = await server.mockRpc({
|
||||
method: "web_read_group",
|
||||
model: "hobbit",
|
||||
groupBy: ["profession"],
|
||||
aggregates: ["__count"],
|
||||
auto_unfold: true,
|
||||
unfold_read_specification: { display_name: {}, age: {}, profession: {} },
|
||||
});
|
||||
expect(result).toEqual({
|
||||
groups: [
|
||||
{
|
||||
__extra_domain: [],
|
||||
profession: "adventurer",
|
||||
__count: 5,
|
||||
__records: server.data.hobbit.records.filter(
|
||||
(r) => r.profession === "adventurer"
|
||||
),
|
||||
},
|
||||
{
|
||||
__extra_domain: [],
|
||||
profession: "brewer",
|
||||
__count: 5,
|
||||
__records: server.data.hobbit.records.filter((r) => r.profession === "brewer"),
|
||||
},
|
||||
{
|
||||
__extra_domain: [],
|
||||
profession: "gardener",
|
||||
__count: 6,
|
||||
__records: server.data.hobbit.records.filter(
|
||||
(r) => r.profession === "gardener"
|
||||
),
|
||||
},
|
||||
],
|
||||
length: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test("'web_read_group': 2 groups", async () => {
|
||||
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"],
|
||||
aggregates: ["__count"],
|
||||
auto_unfold: true,
|
||||
unfold_read_specification: { display_name: {}, age: {} },
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.groups).toHaveLength(2);
|
||||
expect(result.groups.map((g) => g.profession)).toEqual(["gardener", "adventurer"]);
|
||||
expect(result.groups.reduce((acc, g) => acc + g.__count, 0)).toBe(MAIN_RECORDSET_SIZE);
|
||||
expect(result.groups.every((g) => g.__count === g.__records.length)).toBe(true);
|
||||
});
|
||||
|
||||
test("'web_read_group': all groups", async () => {
|
||||
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"],
|
||||
aggregates: ["__count"],
|
||||
auto_unfold: true,
|
||||
unfold_read_specification: { display_name: {}, age: {} },
|
||||
});
|
||||
expect(result.length).toBe(3);
|
||||
expect(result.groups).toHaveLength(3);
|
||||
expect(result.groups.map((g) => g.profession)).toEqual([
|
||||
"gardener",
|
||||
"brewer",
|
||||
"adventurer",
|
||||
]);
|
||||
expect(result.groups.reduce((acc, g) => acc + g.__count, 0)).toBe(MAIN_RECORDSET_SIZE);
|
||||
expect(result.groups.every((g) => g.__count === g.__records.length)).toBe(true);
|
||||
});
|
||||
|
||||
test("'web_read_group': 'max' aggregator", async () => {
|
||||
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
|
||||
const result = await server.mockRpc({
|
||||
method: "web_read_group",
|
||||
model: "res.users",
|
||||
groupBy: ["name"],
|
||||
aggregates: ["age:max", "height:min"],
|
||||
});
|
||||
// didn't crash, but we can't assert the aggregate values as they are non deterministic,
|
||||
// and we don't really mind actually (max/min aren't even implemented, they behave as sum)
|
||||
expect(result.length).toEqual(5);
|
||||
});
|
||||
|
||||
test("'formatted_read_group': groupBy", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const result = await server.mockRpc({
|
||||
method: "formatted_read_group",
|
||||
model: "hobbit",
|
||||
groupBy: ["profession"],
|
||||
aggregates: ["__count"],
|
||||
});
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((g) => g.profession)).toEqual(["adventurer", "brewer", "gardener"]);
|
||||
expect(result.reduce((acc, g) => acc + g.__count, 0)).toBe(MAIN_RECORDSET_SIZE);
|
||||
});
|
||||
|
||||
test("'formatted_read_group': groupBy and field", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const result = await server.mockRpc({
|
||||
method: "formatted_read_group",
|
||||
model: "hobbit",
|
||||
groupBy: ["profession"],
|
||||
aggregates: ["__count", "age:sum"],
|
||||
});
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((g) => g.profession)).toEqual(["adventurer", "brewer", "gardener"]);
|
||||
expect(result.reduce((acc, g) => acc + g.__count, 0)).toBe(MAIN_RECORDSET_SIZE);
|
||||
expect(result.reduce((acc, g) => acc + g["age:sum"], 0)).toBe(
|
||||
server.data.hobbit.records.reduce((acc, rec) => acc + rec.age, 0)
|
||||
);
|
||||
});
|
||||
|
||||
test("'formatted_read_group': multiple groupBys", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const result = await server.mockRpc({
|
||||
method: "formatted_read_group",
|
||||
model: "hobbit",
|
||||
groupBy: ["profession", "age"],
|
||||
});
|
||||
expect(result[0]).toInclude("profession");
|
||||
expect(result[0]).toInclude("age");
|
||||
});
|
||||
|
||||
test("'formatted_read_group': multiple groupBys among which a many2many", async () => {
|
||||
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
|
||||
const result = await server.mockRpc({
|
||||
method: "formatted_read_group",
|
||||
model: "res.users",
|
||||
groupBy: ["height", "tag_ids"],
|
||||
});
|
||||
expect(result[0].tag_ids[0]).toBeOfType("number");
|
||||
expect(result[0].tag_ids[1]).toBeOfType("string");
|
||||
});
|
||||
|
||||
test("'read': no id", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const result = await server.mockRpc({
|
||||
method: "read",
|
||||
model: "hobbit",
|
||||
args: [[], ["display_name"]],
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test("'read': one id", async () => {
|
||||
const server = new DeterministicSampleServer("hobbit", fields.hobbit);
|
||||
const result = await server.mockRpc({
|
||||
method: "read",
|
||||
model: "hobbit",
|
||||
args: [[1], ["display_name"]],
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].display_name).toMatch(/\w+/);
|
||||
expect(result[0].id).toBe(1);
|
||||
});
|
||||
|
||||
test("'read': more than all available ids", async () => {
|
||||
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"]],
|
||||
});
|
||||
expect(result).toHaveLength(MAIN_RECORDSET_SIZE);
|
||||
});
|
||||
|
||||
test("'formatted_read_group': partial support of array_agg", async () => {
|
||||
fields["res.users"].id = { type: "integer", name: "ID" };
|
||||
const server = new DeterministicSampleServer("res.users", fields["res.users"]);
|
||||
const result = await server.mockRpc({
|
||||
method: "formatted_read_group",
|
||||
model: "res.users",
|
||||
aggregates: ["id:array_agg"],
|
||||
groupBy: [],
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
const ids = new Array(16).fill(0).map((_, index) => index + 1);
|
||||
expect(result[0]["id:array_agg"]).toEqual(ids);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue