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 { Record } from "@web/model/record"; 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`
hello
`; } onRpc(({ route }) => expect.step(route)); await mountWithCleanup(Parent); expect(queryFirst`.root`).toHaveOuterHTML(`
hello
yop
`); 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` `; 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(`predefined fields and values`, async () => { class Parent extends Component { static props = ["*"]; static components = { Record, Field }; static template = xml` `; 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(`provides a way to handle changes in the record`, async () => { class Parent extends Component { static props = ["*"]; static components = { Record, Field }; static template = xml` `; setup() { this.fields = { foo: { name: "foo", type: "char", }, bar: { name: "bar", type: "boolean", }, }; this.values = { foo: "abc", bar: true, }; } 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(`provides a way to handle before/after saved the record`, async () => { class Parent extends Component { static props = ["*"]; static components = { Record, Field }; static template = xml` `; 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.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` `; setup() { this.fields = { foo: { name: "foo", type: "many2one", relation: "bar", }, }; this.values = { foo: [1, "bar1"], }; } onRecordChanged(record, changes) { expect.step("record changed"); expect(changes).toEqual({ foo: 3 }); expect(record.data).toEqual({ foo: [3, "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/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` `; 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 array 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` `; 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 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` `; 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` `; setup() { this.fields = { foo: { name: "foo", type: "char", }, bar: { name: "bar", type: "boolean", }, }; this.values = useState({ foo: "abc", bar: true, }); } 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` `; 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` `; 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`
`; 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"` ); });