mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 09:52:02 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
|
|
@ -1,128 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { getFixture, triggerEvents } from "@web/../tests/helpers/utils";
|
||||
import { pagerNext } from "@web/../tests/search/helpers";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { fakeCookieService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "text",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: `yop` },
|
||||
{ id: 2, foo: `blip` },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("cookie", fakeCookieService);
|
||||
});
|
||||
|
||||
QUnit.module("AceEditorField");
|
||||
|
||||
QUnit.test("AceEditorField on text fields works", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok("ace" in window, "the ace library should be loaded");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.ace_content",
|
||||
"should have rendered something with ace editor"
|
||||
);
|
||||
|
||||
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
|
||||
});
|
||||
|
||||
QUnit.test("AceEditorField doesn't crash when editing", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await triggerEvents(target, ".ace-view-editor textarea", ["focus", "click"]);
|
||||
assert.hasClass(target.querySelector(".ace-view-editor"), "ace_focus");
|
||||
});
|
||||
|
||||
QUnit.test("AceEditorField is updated on value change", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
|
||||
|
||||
await pagerNext(target);
|
||||
|
||||
assert.ok(target.querySelector(".o_field_ace").textContent.includes("blip"));
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"leaving an untouched record with an unset ace field should not write",
|
||||
async (assert) => {
|
||||
serverData.models.partner.records.forEach((rec) => {
|
||||
rec.foo = false;
|
||||
});
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method) {
|
||||
assert.step(`${args.method}: ${JSON.stringify(args.args)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["get_views: []", 'read: [[1],["foo","display_name"]]']);
|
||||
await pagerNext(target);
|
||||
assert.verifySteps(['read: [[2],["foo","display_name"]]']);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
/* global ace */
|
||||
|
||||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
editAce,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
pagerNext,
|
||||
preloadBundle,
|
||||
preventResizeObserverError,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
_rec_name = "display_name";
|
||||
|
||||
foo = fields.Text({ default: "My little Foo Value" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, foo: "yop" },
|
||||
{ id: 2, foo: "blip" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
preloadBundle("web.ace_lib");
|
||||
preventResizeObserverError();
|
||||
|
||||
test("AceEditorField on text fields works", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect(window).toInclude("ace", { message: "the ace library should be loaded" });
|
||||
expect(`div.ace_content`).toHaveCount(1);
|
||||
expect(".o_field_code").toHaveText(/yop/);
|
||||
});
|
||||
|
||||
test("AceEditorField mark as dirty as soon at onchange", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
|
||||
const aceEditor = queryOne`.ace_editor`;
|
||||
expect(aceEditor).toHaveText(/yop/);
|
||||
|
||||
// edit the foo field
|
||||
ace.edit(aceEditor).setValue("blip");
|
||||
await animationFrame();
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).not.toHaveClass("invisible");
|
||||
|
||||
// revert edition
|
||||
ace.edit(aceEditor).setValue("yop");
|
||||
await animationFrame();
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveClass("invisible");
|
||||
});
|
||||
|
||||
test("AceEditorField on html fields works", async () => {
|
||||
Partner._fields.html_field = fields.Html();
|
||||
Partner._records.push({ id: 3, html_field: `<p>My little HTML Test</p>` });
|
||||
|
||||
onRpc(({ method }) => expect.step(method));
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 3,
|
||||
type: "form",
|
||||
arch: `<form><field name="html_field" widget="code" /></form>`,
|
||||
});
|
||||
expect(".o_field_code").toHaveText(/My little HTML Test/);
|
||||
expect.verifySteps(["get_views", "web_read"]);
|
||||
|
||||
// Modify foo and save
|
||||
await editAce("DEF");
|
||||
await clickSave();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test.tags("desktop", "focus required");
|
||||
test("AceEditorField doesn't crash when editing", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
|
||||
await contains(".ace_editor .ace_content").click();
|
||||
expect(".ace-view-editor").toHaveClass("ace_focus");
|
||||
});
|
||||
|
||||
test("AceEditorField is updated on value change", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect(".o_field_code").toHaveText(/yop/);
|
||||
|
||||
await pagerNext();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".o_field_code").toHaveText(/blip/);
|
||||
});
|
||||
|
||||
test("leaving an untouched record with an unset ace field should not write", async () => {
|
||||
for (const record of Partner._records) {
|
||||
record.foo = false;
|
||||
}
|
||||
|
||||
onRpc(({ args, method }) => {
|
||||
if (method) {
|
||||
expect.step(`${method}: ${JSON.stringify(args)}`);
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views: []", "web_read: [[1]]"]);
|
||||
|
||||
await pagerNext();
|
||||
expect.verifySteps(["web_read: [[2]]"]);
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("AceEditorField only trigger onchanges when blurred", async () => {
|
||||
Partner._onChanges.foo = () => {};
|
||||
for (const record of Partner._records) {
|
||||
record.foo = false;
|
||||
}
|
||||
|
||||
onRpc(({ args, method }) => {
|
||||
expect.step(`${method}: ${JSON.stringify(args)}`);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
type: "form",
|
||||
arch: `<form><field name="display_name"/><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views: []", "web_read: [[1]]"]);
|
||||
|
||||
await editAce("a");
|
||||
await contains(getFixture()).focus(); // blur ace editor
|
||||
expect.verifySteps([`onchange: [[1],{"foo":"a"},["foo"],{"display_name":{},"foo":{}}]`]);
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps([`web_save: [[1],{"foo":"a"}]`]);
|
||||
});
|
||||
|
||||
test("Save and Discard buttons are displayed when necessary", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="foo" widget="code"/></form>`,
|
||||
});
|
||||
|
||||
await editAce("a");
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).not.toHaveClass("invisible");
|
||||
await clickSave();
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveCount(1);
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveClass("invisible");
|
||||
});
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
_rec_name = "display_name";
|
||||
|
||||
many2one_field = fields.Many2one({ relation: "res.partner" });
|
||||
selection_field = fields.Selection({
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
many2one_field: 4,
|
||||
selection_field: "blocked",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
many2one_field: 1,
|
||||
selection_field: "normal",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
display_name: "", // empty value
|
||||
selection_field: "done",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "fourth record",
|
||||
selection_field: "done",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("BadgeField component on a char field in list view", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="display_name" widget="badge"/></list>`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="display_name"]:contains(first record)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"]:contains(second record)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"]:contains(fourth record)`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("BadgeField component on a selection field in list view", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="selection_field" widget="badge"/></list>`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="selection_field"]:contains(Blocked)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="selection_field"]:contains(Normal)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="selection_field"]:contains(Done)`).toHaveCount(2);
|
||||
});
|
||||
|
||||
test("BadgeField component on a many2one field in list view", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="many2one_field" widget="badge"/></list>`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="many2one_field"]:contains(first record)`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="many2one_field"]:contains(fourth record)`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("BadgeField component with decoration-xxx attributes", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="selection_field" widget="badge"/>
|
||||
<field name="display_name" widget="badge" decoration-danger="selection_field == 'done'" decoration-warning="selection_field == 'blocked'"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="display_name"]`).toHaveCount(4);
|
||||
expect(`.o_field_badge[name="display_name"] .text-bg-danger`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"] .text-bg-warning`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("BadgeField component with color_field option", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="id" column_invisible="1"/>
|
||||
<field name="display_name" widget="badge" options="{'color_field': 'id'}"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`.o_field_badge[name="display_name"]`).toHaveCount(4);
|
||||
expect(`.o_field_badge[name="display_name"] .o_badge_color_1`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"] .o_badge_color_2`).toHaveCount(1);
|
||||
expect(`.o_field_badge[name="display_name"] .o_badge_color_3`).toHaveCount(0); //empty value
|
||||
expect(`.o_field_badge[name="display_name"] .o_badge_color_4`).toHaveCount(1);
|
||||
});
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: {
|
||||
string: "Char Field",
|
||||
type: "char",
|
||||
default: "Default char value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
many2one_field: {
|
||||
string: "Many2one Field",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
selection_field: {
|
||||
string: "Selection",
|
||||
type: "selection",
|
||||
searchable: true,
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
many2one_field: 4,
|
||||
selection_field: "blocked",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
many2one_field: 1,
|
||||
selection_field: "normal",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
display_name: "", // empty value
|
||||
selection_field: "done",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "fourth record",
|
||||
selection_field: "done",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("BadgeField");
|
||||
|
||||
QUnit.test("BadgeField component on a char field in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="display_name" widget="badge"/></list>',
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"]:contains(first record)');
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"]:contains(second record)');
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"]:contains(fourth record)');
|
||||
});
|
||||
|
||||
QUnit.test("BadgeField component on a selection field in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="selection_field" widget="badge"/></list>',
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_field_badge[name="selection_field"]:contains(Blocked)');
|
||||
assert.containsOnce(target, '.o_field_badge[name="selection_field"]:contains(Normal)');
|
||||
assert.containsN(target, '.o_field_badge[name="selection_field"]:contains(Done)', 2);
|
||||
});
|
||||
|
||||
QUnit.test("BadgeField component on a many2one field in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="many2one_field" widget="badge"/></list>',
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_field_badge[name="many2one_field"]:contains(first record)');
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_badge[name="many2one_field"]:contains(fourth record)'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("BadgeField component with decoration-xxx attributes", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="selection_field" widget="badge"/>
|
||||
<field name="display_name" widget="badge" decoration-danger="selection_field == 'done'" decoration-warning="selection_field == 'blocked'"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
assert.containsN(target, '.o_field_badge[name="display_name"]', 4);
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"] .text-bg-danger');
|
||||
assert.containsOnce(target, '.o_field_badge[name="display_name"] .text-bg-warning');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
MockServer,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
color = fields.Selection({
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
});
|
||||
product_color_id = fields.Integer({
|
||||
relation: "product",
|
||||
related: "product_id.color",
|
||||
default: 20,
|
||||
});
|
||||
|
||||
_records = [{ id: 1 }, { id: 2, product_id: 37, product_color_id: 6 }];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
_rec_name = "display_name";
|
||||
|
||||
name = fields.Char("name");
|
||||
color = fields.Integer("color");
|
||||
|
||||
_records = [
|
||||
{ id: 37, display_name: "xphone", name: "xphone", color: 6 },
|
||||
{ id: 41, display_name: "xpad", name: "xpad", color: 7 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("BadgeSelectionField widget on a many2one in a new record", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(`saved product_id: ${args[1]["product_id"]}`);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="product_id" widget="selection_badge"/></form>`,
|
||||
});
|
||||
|
||||
expect(`div.o_field_selection_badge`).toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect(`span.o_selection_badge`).toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect(`span.o_selection_badge:contains(xphone)`).toHaveCount(1, {
|
||||
message: "one of them should be xphone",
|
||||
});
|
||||
expect(`span.active`).toHaveCount(0, { message: "none of the input should be checked" });
|
||||
|
||||
await contains(`span.o_selection_badge`).click();
|
||||
expect(`span.active`).toHaveCount(1, { message: "one of the input should be checked" });
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps(["saved product_id: 37"]);
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection in a new record", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(`saved color: ${args[1]["color"]}`);
|
||||
});
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="color" widget="selection_badge"/></form>`,
|
||||
});
|
||||
|
||||
expect(`div.o_field_selection_badge`).toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect("span.o_selection_badge").toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect(`span.o_selection_badge:contains(Red)`).toHaveCount(1, {
|
||||
message: "one of them should be Red",
|
||||
});
|
||||
|
||||
await contains(`span.o_selection_badge:last`).click();
|
||||
await clickSave();
|
||||
expect.verifySteps(["saved color: black"]);
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection in a readonly mode", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="color" widget="selection_badge" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`div.o_readonly_modifier span`).toHaveCount(1, {
|
||||
message: "should have 1 possible value in readonly mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection unchecking selected value", async () => {
|
||||
onRpc("res.partner", "web_save", ({ args }) => {
|
||||
expect.step("web_save");
|
||||
expect(args[1]).toEqual({ color: false });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
expect("div.o_field_selection_badge").toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect("span.o_selection_badge").toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect("span.o_selection_badge.active").toHaveCount(1, { message: "one is active" });
|
||||
expect("span.o_selection_badge.active").toHaveText("Red", {
|
||||
message: "the active one should be Red",
|
||||
});
|
||||
|
||||
// click again on red option and save to update the server data
|
||||
await contains("span.o_selection_badge.active").click();
|
||||
expect.verifySteps([]);
|
||||
await contains(".o_form_button_save").click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
|
||||
expect(MockServer.env["res.partner"].at(-1).color).toBe(false, {
|
||||
message: "the new value should be false as we have selected same value as default",
|
||||
});
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget on a selection unchecking selected value (required field)", async () => {
|
||||
Partner._fields.color.required = true;
|
||||
onRpc("res.partner", "web_save", ({ args }) => {
|
||||
expect.step("web_save");
|
||||
expect(args[1]).toEqual({ color: "red" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
expect("div.o_field_selection_badge").toHaveCount(1, {
|
||||
message: "should have rendered outer div",
|
||||
});
|
||||
expect("span.o_selection_badge").toHaveCount(2, { message: "should have 2 possible choices" });
|
||||
expect("span.o_selection_badge.active").toHaveCount(1, { message: "one is active" });
|
||||
expect("span.o_selection_badge.active").toHaveText("Red", {
|
||||
message: "the active one should be Red",
|
||||
});
|
||||
|
||||
// click again on red option and save to update the server data
|
||||
await contains("span.o_selection_badge.active").click();
|
||||
expect.verifySteps([]);
|
||||
await contains(".o_form_button_save").click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
|
||||
expect(MockServer.env["res.partner"].at(-1).color).toBe("red", {
|
||||
message: "the new value should be red",
|
||||
});
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget in list with the color_field option", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="product_color_id" invisible="1"/>
|
||||
<field name="product_id" widget="selection_badge" options="{'color_field': 'product_color_id'}"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
|
||||
// Ensure that the correct o_badge_color is used.
|
||||
expect(`.o_field_selection_badge[name="product_id"] .o_badge_color_6`).toHaveCount(1);
|
||||
expect(`.o_field_selection_badge[name="product_id"] .o_badge_color_7`).toHaveCount(0);
|
||||
expect(`div.o_field_selection_badge span:contains(xphone)`).toHaveCount(1);
|
||||
expect(`div.o_field_selection_badge span:contains(xpad)`).toHaveCount(0);
|
||||
|
||||
// Open the M2O selection.
|
||||
await contains(`.o_field_selection_badge[name="product_id"] .o_badge_color_6`).click();
|
||||
|
||||
// Ensure that the 'badge' display is used.
|
||||
expect("span.btn-secondary.badge").toHaveCount(2);
|
||||
expect(`span.btn-secondary.active:contains(xphone)`).toHaveCount(1);
|
||||
expect(`span.btn-secondary.active:contains(xpad)`).toHaveCount(0);
|
||||
|
||||
// Select the second product.
|
||||
await contains(`span.btn-secondary:contains(xpad)`).click();
|
||||
|
||||
expect(`span.btn-secondary.active:contains(xphone)`).toHaveCount(0);
|
||||
expect(`span.btn-secondary.active:contains(xpad)`).toHaveCount(1);
|
||||
|
||||
// Save changes.
|
||||
await contains(".o_list_button_save").click();
|
||||
|
||||
expect(`.o_field_selection_badge[name="product_id"] .o_badge_color_6`).toHaveCount(0);
|
||||
expect(`.o_field_selection_badge[name="product_id"] .o_badge_color_7`).toHaveCount(1);
|
||||
expect(`div.o_field_selection_badge span:contains(xphone)`).toHaveCount(0);
|
||||
expect(`div.o_field_selection_badge span:contains(xpad)`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("BadgeSelectionField widget in list without the color_field option", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="id"/>
|
||||
<field name="product_id" widget="selection_badge"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
// Ensure that the 'btn btn-secondary' display is used instead of the 'o_badge_color' one.
|
||||
expect(`div.o_field_selection_badge span.btn-secondary`).toHaveCount(1);
|
||||
expect(`div.o_field_selection_badge span.btn-secondary:contains(xphone)`).toHaveCount(1);
|
||||
expect(`div.o_field_selection_badge span.btn-secondary:contains(xpad)`).toHaveCount(0);
|
||||
|
||||
// Open the M2O selection.
|
||||
await contains(`div.o_field_selection_badge span:contains(xphone)`).click();
|
||||
|
||||
// Ensure that the 'badge' display is used.
|
||||
expect("span.btn-secondary.badge").toHaveCount(2);
|
||||
expect(`span.btn-secondary.active:contains(xphone)`).toHaveCount(1);
|
||||
expect(`span.btn-secondary.active:contains(xpad)`).toHaveCount(0);
|
||||
|
||||
// Select the second product.
|
||||
await contains(`span.btn-secondary:contains(xpad)`).click();
|
||||
|
||||
expect(`span.btn-secondary.active:contains(xphone)`).toHaveCount(0);
|
||||
expect(`span.btn-secondary.active:contains(xpad)`).toHaveCount(1);
|
||||
|
||||
// Save changes.
|
||||
await contains(".o_list_button_save").click();
|
||||
|
||||
expect(`div.o_field_selection_badge span.btn-secondary`).toHaveCount(1);
|
||||
expect(`div.o_field_selection_badge span.btn-secondary:contains(xphone)`).toHaveCount(0);
|
||||
expect(`div.o_field_selection_badge span.btn-secondary:contains(xpad)`).toHaveCount(1);
|
||||
});
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
product_id: { string: "Product", type: "many2one", relation: "product" },
|
||||
color: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["red", "Red"],
|
||||
["black", "Black"],
|
||||
],
|
||||
default: "red",
|
||||
string: "Color",
|
||||
},
|
||||
reference: {
|
||||
string: "Reference Field",
|
||||
type: "reference",
|
||||
selection: [
|
||||
["product", "Product"],
|
||||
["partner_type", "Partner Type"],
|
||||
["partner", "Partner"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
reference: "product,37",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
product_id: 37,
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BadgeSelectionField");
|
||||
|
||||
QUnit.test("BadgeSelectionField widget on a many2one in a new record", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="product_id" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_selection_badge",
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector("span.o_selection_badge").textContent,
|
||||
"xphone",
|
||||
"one of them should be xphone"
|
||||
);
|
||||
assert.containsNone(target, "span.active", "none of the input should be checked");
|
||||
|
||||
await click(target.querySelector("span.o_selection_badge"));
|
||||
|
||||
assert.containsOnce(target, "span.active", "one of the input should be checked");
|
||||
|
||||
await click(target, ".o_form_button_save");
|
||||
|
||||
var newRecord = _.last(serverData.models.partner.records);
|
||||
assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"BadgeSelectionField widget on a selection in a new record",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_selection_badge",
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector("span.o_selection_badge").textContent,
|
||||
"Red",
|
||||
"one of them should be Red"
|
||||
);
|
||||
|
||||
// click on 2nd option
|
||||
await click(target.querySelector("span.o_selection_badge:last-child"));
|
||||
|
||||
await click(target.querySelector(".o_form_button_save"));
|
||||
|
||||
var newRecord = _.last(serverData.models.partner.records);
|
||||
assert.strictEqual(
|
||||
newRecord.color,
|
||||
"black",
|
||||
"should have saved record with correct value"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"BadgeSelectionField widget on a selection in a readonly mode",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="color" widget="selection_badge" readonly="1"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_readonly_modifier span",
|
||||
"should have 1 possible value in readonly mode"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"BadgeSelectionField widget on a selection unchecking selected value",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="color" widget="selection_badge"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_selection_badge",
|
||||
"should have rendered outer div"
|
||||
);
|
||||
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
|
||||
assert.strictEqual(
|
||||
target.querySelector("span.o_selection_badge").textContent,
|
||||
"Red",
|
||||
"one of them should be Red"
|
||||
);
|
||||
|
||||
// click again on red option
|
||||
await click(target.querySelector("span.o_selection_badge.active"));
|
||||
|
||||
await click(target.querySelector(".o_form_button_save"));
|
||||
|
||||
var newRecord = _.last(serverData.models.partner.records);
|
||||
assert.strictEqual(
|
||||
newRecord.color,
|
||||
false,
|
||||
"the new value should be false as we have selected same value as default"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
is_raining_outside = fields.Boolean();
|
||||
mood = fields.Selection({
|
||||
selection: [
|
||||
["happy", "Happy"],
|
||||
["sad", "Sad"],
|
||||
],
|
||||
});
|
||||
color = fields.Selection({
|
||||
selection: [
|
||||
["white", "White"],
|
||||
["grey", "Grey"],
|
||||
["black", "Black"],
|
||||
],
|
||||
});
|
||||
allowed_colors = fields.Json();
|
||||
allowed_moods = fields.Json();
|
||||
|
||||
_onChanges = {
|
||||
is_raining_outside(record) {
|
||||
record.allowed_moods = ["happy"] + (record.is_raining_outside ? ["sad"] : []);
|
||||
},
|
||||
color(record) {
|
||||
record.allowed_moods =
|
||||
(record.color !== "black" ? ["happy"] : []) +
|
||||
(record.color !== "white" ? ["sad"] : []);
|
||||
},
|
||||
mood(record) {
|
||||
record.allowed_colors =
|
||||
(record.mood === "happy" ? ["white"] : []) +
|
||||
["grey"] +
|
||||
(record.mood === "sad" ? ["black"] : []);
|
||||
},
|
||||
};
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
allowed_colors: "['white', 'grey']",
|
||||
allowed_moods: "['happy']",
|
||||
display_name: "first record",
|
||||
is_raining_outside: false,
|
||||
mood: "happy",
|
||||
color: "white",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("badge selection field with filter, empty list", async () => {
|
||||
Partner._records[0].allowed_colors = [];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="allowed_colors" invisible="1"/>
|
||||
<field name="color" widget="selection_badge_with_filter"
|
||||
options="{'allowed_selection_field': 'allowed_colors'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_selection_badge").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("badge selection field with filter, single choice", async () => {
|
||||
Partner._records[0].allowed_colors = ["grey"];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="allowed_colors" invisible="1"/>
|
||||
<field name="color" widget="selection_badge_with_filter"
|
||||
options="{'allowed_selection_field': 'allowed_colors'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_selection_badge").toHaveCount(1);
|
||||
expect(".o_selection_badge[value='\"white\"']").toHaveCount(0);
|
||||
expect(".o_selection_badge[value='\"grey\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"black\"']").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("badge selection field with filter, all choices", async () => {
|
||||
Partner._records[0].allowed_colors = ["white", "grey", "black"];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="allowed_colors" invisible="1"/>
|
||||
<field name="color" widget="selection_badge_with_filter"
|
||||
options="{'allowed_selection_field': 'allowed_colors'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_selection_badge").toHaveCount(3);
|
||||
expect(".o_selection_badge[value='\"white\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"grey\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"black\"']").toBeVisible();
|
||||
});
|
||||
|
||||
test("badge selection field with filter, synchronize with other field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="is_raining_outside"/>
|
||||
<field name="allowed_moods" invisible="1"/>
|
||||
<field name="mood" widget="selection_badge_with_filter"
|
||||
options="{'allowed_selection_field': 'allowed_moods'}"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
// not raining outside => sad should be invisible
|
||||
expect("[name='is_raining_outside'] input").not.toBeChecked();
|
||||
expect("div[name='mood'] .o_selection_badge").toHaveCount(1);
|
||||
expect(".o_selection_badge[value='\"happy\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"sad\"']").toHaveCount(0);
|
||||
|
||||
await click("[name='is_raining_outside'] input");
|
||||
await animationFrame();
|
||||
|
||||
// raining outside => sad should be visible
|
||||
expect("[name='is_raining_outside'] input").toBeChecked();
|
||||
expect("div[name='mood'] .o_selection_badge").toHaveCount(2);
|
||||
expect(".o_selection_badge[value='\"happy\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"sad\"']").toBeVisible();
|
||||
|
||||
await click("[name='is_raining_outside'] input");
|
||||
await animationFrame();
|
||||
|
||||
// not raining outside => sad should be invisible
|
||||
expect("[name='is_raining_outside'] input").not.toBeChecked();
|
||||
expect("div[name='mood'] .o_selection_badge").toHaveCount(1);
|
||||
expect(".o_selection_badge[value='\"happy\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"sad\"']").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("badge selection field with filter, cross badge synchronization", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="allowed_moods"/>
|
||||
<field name="allowed_colors"/>
|
||||
<field name="mood" widget="selection_badge_with_filter"
|
||||
options="{'allowed_selection_field': 'allowed_moods'}"/>
|
||||
<field name="color" widget="selection_badge_with_filter"
|
||||
options="{'allowed_selection_field': 'allowed_colors'}"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// happy and white by default, sad and black should be invisible
|
||||
expect("div[name='mood'] .o_selection_badge").toHaveCount(1);
|
||||
expect("div[name='color'] .o_selection_badge").toHaveCount(2);
|
||||
expect(".o_selection_badge[value='\"happy\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"sad\"']").toHaveCount(0);
|
||||
expect(".o_selection_badge[value='\"white\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"grey\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"black\"']").toHaveCount(0);
|
||||
|
||||
await click(".o_selection_badge[value='\"grey\"']");
|
||||
await animationFrame();
|
||||
|
||||
// happy and grey, sad should be revealed
|
||||
expect("div[name='mood'] .o_selection_badge").toHaveCount(2);
|
||||
expect("div[name='color'] .o_selection_badge").toHaveCount(2);
|
||||
expect(".o_selection_badge[value='\"happy\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"sad\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"white\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"grey\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"black\"']").toHaveCount(0);
|
||||
|
||||
await click(".o_selection_badge[value='\"sad\"']");
|
||||
await animationFrame();
|
||||
|
||||
// sad and grey, white should disappear and black should appear
|
||||
expect("div[name='mood'] .o_selection_badge").toHaveCount(2);
|
||||
expect("div[name='color'] .o_selection_badge").toHaveCount(2);
|
||||
expect(".o_selection_badge[value='\"happy\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"sad\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"white\"']").toHaveCount(0);
|
||||
expect(".o_selection_badge[value='\"grey\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"black\"']").toBeVisible();
|
||||
|
||||
await click(".o_selection_badge[value='\"black\"']");
|
||||
await animationFrame();
|
||||
|
||||
// sad and black, happy should disappear
|
||||
expect("div[name='mood'] .o_selection_badge").toHaveCount(1);
|
||||
expect("div[name='color'] .o_selection_badge").toHaveCount(2);
|
||||
expect(".o_selection_badge[value='\"happy\"']").toHaveCount(0);
|
||||
expect(".o_selection_badge[value='\"sad\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"white\"']").toHaveCount(0);
|
||||
expect(".o_selection_badge[value='\"grey\"']").toBeVisible();
|
||||
expect(".o_selection_badge[value='\"black\"']").toBeVisible();
|
||||
});
|
||||
|
|
@ -0,0 +1,487 @@
|
|||
import { after, expect, test } from "@odoo/hoot";
|
||||
import { click, queryOne, queryValue, setInputFiles, waitFor } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
makeServerError,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
pagerNext,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { toBase64Length } from "@web/core/utils/binary";
|
||||
import { MAX_FILENAME_SIZE_BYTES } from "@web/views/fields/binary/binary_field";
|
||||
|
||||
const BINARY_FILE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
foo = fields.Char({ default: "My little Foo Value" });
|
||||
document = fields.Binary();
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
|
||||
_records = [{ foo: "coucou.txt", document: "coucou==\n" }];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 37, name: "xphone" },
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("BinaryField is correctly rendered (readonly)", async () => {
|
||||
onRpc("/web/content", async (request) => {
|
||||
expect.step("/web/content");
|
||||
|
||||
const body = await request.text();
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("field")).toBe("document", {
|
||||
message: "we should download the field document",
|
||||
});
|
||||
expect(body.get("data")).toBe("coucou==\n", {
|
||||
message: "we should download the correct data",
|
||||
});
|
||||
|
||||
return new Blob([body.get("data")], { type: "text/plain" });
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(1, {
|
||||
message: "the binary field should be rendered as a downloadable link in readonly",
|
||||
});
|
||||
expect(`.o_field_widget[name="document"]`).toHaveText("coucou.txt", {
|
||||
message: "the binary field should display the name of the file in the link",
|
||||
});
|
||||
expect(`.o_field_char`).toHaveText("coucou.txt", {
|
||||
message: "the filename field should have the file name as value",
|
||||
});
|
||||
|
||||
// Testing the download button in the field
|
||||
// We must avoid the browser to download the file effectively
|
||||
const deferred = new Deferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
after(() => document.removeEventListener("click", downloadOnClick));
|
||||
|
||||
await contains(`.o_field_widget[name="document"] a`).click();
|
||||
await deferred;
|
||||
expect.verifySteps(["/web/content"]);
|
||||
});
|
||||
|
||||
test("BinaryField is correctly rendered", async () => {
|
||||
onRpc("/web/content", async (request) => {
|
||||
expect.step("/web/content");
|
||||
|
||||
const body = await request.text();
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("field")).toBe("document", {
|
||||
message: "we should download the field document",
|
||||
});
|
||||
expect(body.get("data")).toBe("coucou==\n", {
|
||||
message: "we should download the correct data",
|
||||
});
|
||||
|
||||
return new Blob([body.get("data")], { type: "text/plain" });
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(0, {
|
||||
message: "the binary field should not be rendered as a downloadable link in edit",
|
||||
});
|
||||
expect(`.o_field_widget[name="document"].o_field_binary .o_input`).toHaveValue("coucou.txt", {
|
||||
message: "the binary field should display the file name in the input edit mode",
|
||||
});
|
||||
expect(`.o_field_binary .o_clear_file_button`).toHaveCount(1, {
|
||||
message: "there shoud be a button to clear the file",
|
||||
});
|
||||
expect(`.o_field_char input`).toHaveValue("coucou.txt", {
|
||||
message: "the filename field should have the file name as value",
|
||||
});
|
||||
|
||||
// Testing the download button in the field
|
||||
// We must avoid the browser to download the file effectively
|
||||
const deferred = new Deferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
after(() => document.removeEventListener("click", downloadOnClick));
|
||||
|
||||
await click(`.fa-download`);
|
||||
await deferred;
|
||||
expect.verifySteps(["/web/content"]);
|
||||
|
||||
await click(`.o_field_binary .o_clear_file_button`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_binary input`).not.toBeVisible({ message: "the input should be hidden" });
|
||||
expect(`.o_field_binary .o_select_file_button`).toHaveCount(1, {
|
||||
message: "there should be a button to upload the file",
|
||||
});
|
||||
expect(`.o_field_char input`).toHaveValue("", {
|
||||
message: "the filename field should be empty since we removed the file",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(0, {
|
||||
message:
|
||||
"the binary field should not render as a downloadable link since we removed the file",
|
||||
});
|
||||
expect(`o_field_widget span`).toHaveCount(0, {
|
||||
message:
|
||||
"the binary field should not display a filename in the link since we removed the file",
|
||||
});
|
||||
});
|
||||
|
||||
test("BinaryField is correctly rendered (isDirty)", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// Simulate a file upload
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await setInputFiles([file]);
|
||||
await waitFor(`.o_form_button_save:visible`);
|
||||
expect(`.o_field_widget[name="document"] .fa-download`).toHaveCount(0, {
|
||||
message:
|
||||
"the binary field should not be rendered as a downloadable since the record is dirty",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_widget[name="document"] .fa-download`).toHaveCount(1, {
|
||||
message:
|
||||
"the binary field should render as a downloadable link since the record is not dirty",
|
||||
});
|
||||
});
|
||||
|
||||
test("file name field is not defined", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="document" filename="foo"/></form>`,
|
||||
});
|
||||
expect(`.o_field_binary`).toHaveText("", {
|
||||
message: "there should be no text since the name field is not in the view",
|
||||
});
|
||||
expect(`.o_field_binary .fa-download`).toBeVisible({
|
||||
message: "download icon should be visible",
|
||||
});
|
||||
});
|
||||
|
||||
test("icons are displayed exactly once", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="document" filename="foo"/></form>`,
|
||||
});
|
||||
expect(queryOne`.o_field_binary .o_select_file_button`).toBeVisible({
|
||||
message: "only one select file icon should be visible",
|
||||
});
|
||||
expect(queryOne`.o_field_binary .o_download_file_button`).toBeVisible({
|
||||
message: "only one download file icon should be visible",
|
||||
});
|
||||
expect(queryOne`.o_field_binary .o_clear_file_button`).toBeVisible({
|
||||
message: "only one clear file icon should be visible",
|
||||
});
|
||||
});
|
||||
|
||||
test("input value is empty when clearing after uploading", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await setInputFiles([file]);
|
||||
await waitFor(`.o_form_button_save:visible`);
|
||||
expect(`.o_field_binary input[type=text]`).toHaveAttribute("readonly");
|
||||
expect(`.o_field_binary input[type=text]`).toHaveValue("fake_file.txt");
|
||||
expect(`.o_field_char input[type=text]`).toHaveValue("fake_file.txt");
|
||||
|
||||
await click(`.o_clear_file_button`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_binary .o_input_file`).toHaveValue("");
|
||||
expect(`.o_field_char input`).toHaveValue("");
|
||||
});
|
||||
|
||||
test("option accepted_file_extensions", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="binary" options="{'accepted_file_extensions': '.dat,.bin'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(`input.o_input_file`).toHaveAttribute("accept", ".dat,.bin", {
|
||||
message: "the input should have the correct ``accept`` attribute",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("readonly in create mode does not download", async () => {
|
||||
onRpc("/web/content", () => {
|
||||
expect.step("We shouldn't be getting the file.");
|
||||
});
|
||||
|
||||
Partner._onChanges.product_id = (record) => {
|
||||
record.document = "onchange==\n";
|
||||
};
|
||||
Partner._fields.document.readonly = true;
|
||||
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="document" filename="yooo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(`.o_field_many2one[name='product_id'] input`);
|
||||
await animationFrame();
|
||||
await click(`.o_field_many2one[name='product_id'] .dropdown-item`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_widget[name="document"] a`).toHaveCount(0, {
|
||||
message: "The link to download the binary should not be present",
|
||||
});
|
||||
expect(`.o_field_widget[name="document"] a > .fa-download`).toHaveCount(0, {
|
||||
message: "The download icon should not be present",
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("BinaryField in list view (formatter)", async () => {
|
||||
Partner._records[0]["document"] = BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `<list><field name="document"/></list>`,
|
||||
});
|
||||
expect(`.o_data_row .o_data_cell`).toHaveText("93.43 Bytes");
|
||||
});
|
||||
|
||||
test("BinaryField in list view with filename", async () => {
|
||||
Partner._records[0]["document"] = BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="document" filename="foo" widget="binary"/>
|
||||
<field name="foo"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(`.o_data_row .o_data_cell`).toHaveText("coucou.txt");
|
||||
});
|
||||
|
||||
test("new record has no download button", async () => {
|
||||
Partner._fields.document.default = BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="document" filename="foo"/></form>`,
|
||||
});
|
||||
expect(`button.fa-download`).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("filename doesn't exceed 255 bytes", async () => {
|
||||
const LARGE_BINARY_FILE = BINARY_FILE.repeat(5);
|
||||
expect((LARGE_BINARY_FILE.length / 4) * 3).toBeGreaterThan(MAX_FILENAME_SIZE_BYTES, {
|
||||
message:
|
||||
"The initial binary file should be larger than max bytes that can represent the filename",
|
||||
});
|
||||
|
||||
Partner._fields.document.default = LARGE_BINARY_FILE;
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `<form><field name="document"/></form>`,
|
||||
});
|
||||
expect(queryValue(`.o_field_binary input[type=text]`)).toHaveLength(
|
||||
toBase64Length(MAX_FILENAME_SIZE_BYTES),
|
||||
{
|
||||
message: "The filename shouldn't exceed the maximum size in bytes in base64",
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("filename is updated when using the pager", async () => {
|
||||
Partner._records.push(
|
||||
{ id: 1, document: "abc", foo: "abc.txt" },
|
||||
{ id: 2, document: "def", foo: "def.txt" }
|
||||
);
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(`.o_field_binary input[type=text]`).toHaveValue("abc.txt", {
|
||||
message: `displayed value should be "abc.txt"`,
|
||||
});
|
||||
|
||||
await pagerNext();
|
||||
expect(`.o_field_binary input[type=text]`).toHaveValue("def.txt", {
|
||||
message: `displayed value should be "def.txt"`,
|
||||
});
|
||||
});
|
||||
|
||||
test("isUploading state should be set to false after upload", async () => {
|
||||
expect.errors(1);
|
||||
|
||||
Partner._records.push({ id: 1 });
|
||||
Partner._onChanges.document = (record) => {
|
||||
if (record.document) {
|
||||
throw makeServerError({ type: "ValidationError" });
|
||||
}
|
||||
};
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="document"/></form>`,
|
||||
});
|
||||
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await setInputFiles([file]);
|
||||
await waitFor(`.o_form_button_save:visible`);
|
||||
await animationFrame();
|
||||
expect.verifyErrors([/RPC_ERROR/]);
|
||||
expect(`.o_select_file_button`).toHaveText("Upload your file");
|
||||
});
|
||||
|
||||
test("should accept file with allowed MIME type and reject others", async () => {
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo" widget="binary" options="{'allowed_mime_type' : 'application/pdf'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const pdfFile = new File(["test"], "fake_pdf.pdf", { type: "application/pdf" });
|
||||
await setInputFiles([pdfFile]);
|
||||
|
||||
await waitFor(`.o_form_button_save:visible`);
|
||||
expect(`.o_field_binary input[type=text]`).toHaveAttribute("readonly");
|
||||
expect(`.o_field_binary input[type=text]`).toHaveValue("fake_pdf.pdf");
|
||||
|
||||
await click(`.o_clear_file_button`);
|
||||
await animationFrame();
|
||||
|
||||
await click(`.o_select_file_button`);
|
||||
await animationFrame();
|
||||
const textFile = new File(["test"], "text_file.txt", { type: "text/plain" });
|
||||
await setInputFiles([textFile]);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_content").toHaveText(
|
||||
"Oops! 'text_file.txt' didn’t upload since its format isn’t allowed."
|
||||
);
|
||||
expect(".o_notification_bar").toHaveClass("bg-danger");
|
||||
});
|
||||
|
||||
test("doesn't crash if value is not a string", async () => {
|
||||
class Dummy extends models.Model {
|
||||
document = fields.Binary()
|
||||
_applyComputesAndValidate() {}
|
||||
}
|
||||
defineModels([Dummy])
|
||||
Dummy._records.push({ id: 1, document: {} });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "dummy",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_binary input").toHaveValue("");
|
||||
});
|
||||
|
|
@ -1,581 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { makeMockXHR } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { RPCError } from "@web/core/network/rpc_service";
|
||||
import { MAX_FILENAME_SIZE_BYTES } from "@web/views/fields/binary/binary_field";
|
||||
import { toBase64Length } from "@web/core/utils/binary";
|
||||
|
||||
const BINARY_FILE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
},
|
||||
document: { string: "Binary", type: "binary" },
|
||||
product_id: {
|
||||
string: "Product",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
foo: "coucou.txt",
|
||||
document: "coucou==\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
display_name: "xpad",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BinaryField");
|
||||
|
||||
QUnit.test("BinaryField is correctly rendered (readonly)", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
async function send(data) {
|
||||
assert.ok(data instanceof FormData);
|
||||
assert.strictEqual(
|
||||
data.get("field"),
|
||||
"document",
|
||||
"we should download the field document"
|
||||
);
|
||||
assert.strictEqual(
|
||||
data.get("data"),
|
||||
"coucou==\n",
|
||||
"we should download the correct data"
|
||||
);
|
||||
|
||||
this.status = 200;
|
||||
this.response = new Blob([data.get("data")], { type: "text/plain" });
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"the binary field should be rendered as a downloadable link in readonly"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="document"]').textContent,
|
||||
"coucou.txt",
|
||||
"the binary field should display the name of the file in the link"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char").textContent,
|
||||
"coucou.txt",
|
||||
"the filename field should have the file name as value"
|
||||
);
|
||||
|
||||
// Testing the download button in the field
|
||||
// We must avoid the browser to download the file effectively
|
||||
const prom = makeDeferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
prom.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
registerCleanup(() => document.removeEventListener("click", downloadOnClick));
|
||||
await click(target.querySelector('.o_field_widget[name="document"] a'));
|
||||
await prom;
|
||||
});
|
||||
|
||||
QUnit.test("BinaryField is correctly rendered", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
async function send(data) {
|
||||
assert.ok(data instanceof FormData);
|
||||
assert.strictEqual(
|
||||
data.get("field"),
|
||||
"document",
|
||||
"we should download the field document"
|
||||
);
|
||||
assert.strictEqual(
|
||||
data.get("data"),
|
||||
"coucou==\n",
|
||||
"we should download the correct data"
|
||||
);
|
||||
|
||||
this.status = 200;
|
||||
this.response = new Blob([data.get("data")], { type: "text/plain" });
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"the binary field should not be rendered as a downloadable link in edit"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="document"].o_field_binary .o_input').value,
|
||||
"coucou.txt",
|
||||
"the binary field should display the file name in the input edit mode"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_binary .o_clear_file_button",
|
||||
"there shoud be a button to clear the file"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input").value,
|
||||
"coucou.txt",
|
||||
"the filename field should have the file name as value"
|
||||
);
|
||||
|
||||
// Testing the download button in the field
|
||||
// We must avoid the browser to download the file effectively
|
||||
const prom = makeDeferred();
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
prom.resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
registerCleanup(() => document.removeEventListener("click", downloadOnClick));
|
||||
await click(target.querySelector(".fa-download"));
|
||||
await prom;
|
||||
|
||||
await click(target.querySelector(".o_field_binary .o_clear_file_button"));
|
||||
|
||||
assert.isNotVisible(
|
||||
target.querySelector(".o_field_binary input"),
|
||||
"the input should be hidden"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_binary .o_select_file_button",
|
||||
"there should be a button to upload the file"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input").value,
|
||||
"",
|
||||
"the filename field should be empty since we removed the file"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"the binary field should not render as a downloadable link since we removed the file"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
"o_field_widget span",
|
||||
"the binary field should not display a filename in the link since we removed the file"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("BinaryField is correctly rendered (isDirty)", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
// Simulate a file upload
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_field_binary .o_input_file", file);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] .fa-download',
|
||||
"the binary field should not be rendered as a downloadable since the record is dirty"
|
||||
);
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_widget[name="document"] .fa-download',
|
||||
"the binary field should render as a downloadable link since the record is not dirty"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("file name field is not defined", async (assert) => {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary").textContent,
|
||||
"",
|
||||
"there should be no text since the name field is not in the view"
|
||||
);
|
||||
assert.isVisible(
|
||||
target,
|
||||
".o_field_binary .o_form_uri fa-download",
|
||||
"download icon should be visible"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"binary fields input value is empty when clearing after uploading",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_field_binary .o_input_file", file);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_binary input[type=text]").hasAttribute("readonly")
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value,
|
||||
"fake_file.txt",
|
||||
'displayed value should be changed to "fake_file.txt"'
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input[type=text]").value,
|
||||
"fake_file.txt",
|
||||
'related value should be changed to "fake_file.txt"'
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_clear_file_button"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary .o_input_file").value,
|
||||
"",
|
||||
"file input value should be empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_char input").value,
|
||||
"",
|
||||
"related value should be empty"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("BinaryField: option accepted_file_extensions", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="binary" options="{'accepted_file_extensions': '.dat,.bin'}"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
".dat,.bin",
|
||||
"the input should have the correct ``accept`` attribute"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"BinaryField that is readonly in create mode does not download",
|
||||
async function (assert) {
|
||||
async function download() {
|
||||
assert.step("We shouldn't be getting the file.");
|
||||
}
|
||||
const MockXHR = makeMockXHR("", download);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
product_id: function (obj) {
|
||||
obj.document = "onchange==\n";
|
||||
},
|
||||
};
|
||||
|
||||
serverData.models.partner.fields.document.readonly = true;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="document" filename="yooo"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(target, ".o_form_button_create");
|
||||
await click(target, ".o_field_many2one[name='product_id'] input");
|
||||
await click(
|
||||
target.querySelector(".o_field_many2one[name='product_id'] .dropdown-item")
|
||||
);
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a',
|
||||
"The link to download the binary should not be present"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_widget[name="document"] a > .fa-download',
|
||||
"The download icon should not be present"
|
||||
);
|
||||
|
||||
assert.verifySteps([], "We shouldn't have passed through steps");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Binary field in list view", async function (assert) {
|
||||
serverData.models.partner.records[0].document = BINARY_FILE;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="document" filename="yooo"/>
|
||||
</tree>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row .o_data_cell").textContent,
|
||||
"93.43 Bytes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Binary field for new record has no download button", async function (assert) {
|
||||
serverData.models.partner.fields.document.default = BINARY_FILE;
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
assert.containsNone(target, "button.fa-download");
|
||||
});
|
||||
|
||||
QUnit.test("Binary filename doesn't exceed 255 bytes", async function (assert) {
|
||||
const LARGE_BINARY_FILE = BINARY_FILE.repeat(5);
|
||||
assert.ok((LARGE_BINARY_FILE.length / 4 * 3) > MAX_FILENAME_SIZE_BYTES,
|
||||
"The initial binary file should be larger than max bytes that can represent the filename");
|
||||
serverData.models.partner.fields.document.default = LARGE_BINARY_FILE;
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value.length,
|
||||
toBase64Length(MAX_FILENAME_SIZE_BYTES),
|
||||
"The filename shouldn't exceed the maximum size in bytes in base64"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("BinaryField filename is updated when using the pager", async function (assert) {
|
||||
serverData.models.partner.records.push(
|
||||
{
|
||||
id: 1,
|
||||
document: "abc",
|
||||
foo: "abc.txt",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
document: "def",
|
||||
foo: "def.txt",
|
||||
}
|
||||
);
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" filename="foo"/>
|
||||
<field name="foo"/>
|
||||
</form>
|
||||
`,
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value,
|
||||
"abc.txt",
|
||||
'displayed value should be "abc.txt"'
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_pager_next"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_binary input[type=text]").value,
|
||||
"def.txt",
|
||||
'displayed value should be changed to "def.txt"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('isUploading state should be set to false after upload', async function(assert) {
|
||||
assert.expect(1);
|
||||
serverData.models.partner.onchanges = {
|
||||
document: function (obj) {
|
||||
if (obj.document) {
|
||||
const error = new RPCError();
|
||||
error.exceptionName = "odoo.exceptions.ValidationError";
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>`,
|
||||
});
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_field_binary .o_input_file", file);
|
||||
assert.equal(
|
||||
target.querySelector(".o_select_file_button").innerText,
|
||||
"UPLOAD YOUR FILE",
|
||||
"displayed value should be upload your file"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("doesn't crash if value is not a string", async (assert) => {
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
document: {},
|
||||
}]
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.equal(
|
||||
target.querySelector(".o_field_binary input").value,
|
||||
""
|
||||
);
|
||||
})
|
||||
});
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllProperties, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ default: true });
|
||||
|
||||
_records = [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("FavoriteField in kanban view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Remove from Favorites", {
|
||||
message: `the label should say "Remove from Favorites"`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
});
|
||||
|
||||
test("FavoriteField saves changes by default", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step("save");
|
||||
expect(args).toEqual([[1], { bar: false }]);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
expect.verifySteps(["save"]);
|
||||
});
|
||||
|
||||
test("FavoriteField does not save if autosave option is set to false", async () => {
|
||||
onRpc("web_save", () => {
|
||||
expect.step("save");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite" options="{'autosave': False}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("FavoriteField in form view", async () => {
|
||||
onRpc("web_save", () => {
|
||||
expect.step("save");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_favorite"/></form>`,
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a`).toHaveText("Remove from Favorites", {
|
||||
message: `the label should say "Remove from Favorites"`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect.verifySteps(["save"]);
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star-o`).toHaveCount(1, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a`).toHaveText("Add to Favorites", {
|
||||
message: `the label should say "Add to Favorites"`,
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
expect.verifySteps(["save"]);
|
||||
expect(`.o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_field_widget .o_favorite > a`).toHaveText("Remove from Favorites", {
|
||||
message: `the label should say "Remove from Favorites"`,
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("FavoriteField in editable list view without label", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="bar" widget="boolean_favorite" nolabel="1" options="{'autosave': False}"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
await contains(`tbody td:not(.o_list_record_selector)`).click();
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_data_row .o_field_widget .o_favorite > a`).click();
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(0, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
|
||||
// save
|
||||
await contains(`.o_list_button_save`).click();
|
||||
expect(`.o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star-o`).toHaveCount(1, {
|
||||
message: "should not be favorite",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("FavoriteField in list has a fixed width if no label", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
Partner._fields.char = fields.Char();
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="bar" widget="boolean_favorite" nolabel="1"/>
|
||||
<field name="bar" widget="boolean_favorite"/>
|
||||
<field name="char"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
const columnWidths = queryAllProperties(".o_list_table thead th", "offsetWidth");
|
||||
const columnLabels = queryAllTexts(".o_list_table thead th");
|
||||
expect(columnWidths[1]).toBe(29);
|
||||
expect(columnLabels[1]).toBe("");
|
||||
expect(columnWidths[2]).toBeGreaterThan(29);
|
||||
expect(columnLabels[2]).toBe("Bar");
|
||||
});
|
||||
|
||||
test("FavoriteField in kanban view with readonly attribute", async () => {
|
||||
onRpc("web_save", () => {
|
||||
expect.step("should not save");
|
||||
});
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
domain: [["id", "=", 1]],
|
||||
type: "kanban",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="bar" widget="boolean_favorite" readonly="1"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should be favorite",
|
||||
});
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a`).toHaveClass("pe-none");
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite`).toHaveClass("o_disabled");
|
||||
expect(`.o_kanban_record .o_field_widget`).toHaveText("");
|
||||
|
||||
// click on favorite
|
||||
await contains(`.o_field_widget .o_favorite`).click();
|
||||
// expect nothing to change since its readonly
|
||||
expect(`.o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star`).toHaveCount(1, {
|
||||
message: "should remain favorite",
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BooleanFavoriteField");
|
||||
|
||||
QUnit.test("FavoriteField in kanban view", async function (assert) {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="bar" widget="boolean_favorite" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "=", 1]],
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target, ".o_field_widget .o_favorite");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_kanban_record .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should not be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
|
||||
" Add to Favorites",
|
||||
'the label should say "Add to Favorites"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FavoriteField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="bar" widget="boolean_favorite" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target, ".o_field_widget .o_favorite");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should not be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Add to Favorites",
|
||||
'the label should say "Add to Favorites"'
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star-o",
|
||||
"should not be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Add to Favorites",
|
||||
'the label should say "Add to Favorites"'
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target, ".o_field_widget .o_favorite");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .o_favorite > a").textContent,
|
||||
" Remove from Favorites",
|
||||
'the label should say "Remove from Favorites"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FavoriteField in editable list view without label", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="bar" widget="boolean_favorite" nolabel="1" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
|
||||
// switch to edit mode
|
||||
await click(target.querySelector("tbody td:not(.o_list_record_selector)"));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should be favorite"
|
||||
);
|
||||
|
||||
// click on favorite
|
||||
await click(target.querySelector(".o_data_row .o_field_widget .o_favorite"));
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star",
|
||||
"should not be favorite"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:first .o_field_widget .o_favorite > a i.fa.fa-star-o",
|
||||
"should not be favorite"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { check, click, press, uncheck } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ default: true });
|
||||
|
||||
_records = [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("boolean field in form view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<label for="bar" string="Awesome checkbox"/>
|
||||
<field name="bar"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
expect(`.o_field_boolean input`).toBeEnabled();
|
||||
|
||||
await uncheck(`.o_field_boolean input`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await check(`.o_field_boolean input`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await uncheck(`.o_field_boolean input`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await click(`.o_form_view label:not(.form-check-label)`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await click(`.o_form_view label:not(.form-check-label)`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).not.toBeChecked();
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
});
|
||||
|
||||
test("boolean field in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `<list editable="bottom"><field name="bar"/></list>`,
|
||||
});
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input`).toHaveCount(5);
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input:checked`).toHaveCount(4);
|
||||
|
||||
// Edit a line
|
||||
const cell = `tr.o_data_row td:not(.o_list_record_selector):first`;
|
||||
expect(`${cell} .o-checkbox input:only`).toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeEnabled();
|
||||
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await animationFrame();
|
||||
expect(`tr.o_data_row:nth-child(1)`).toHaveClass("o_selected_row", {
|
||||
message: "the row is now selected, in edition",
|
||||
});
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).toBeEnabled();
|
||||
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await click(cell);
|
||||
await animationFrame();
|
||||
expect(`${cell} .o-checkbox input:only`).toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).toBeEnabled();
|
||||
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await animationFrame();
|
||||
|
||||
await click(`.o_list_button_save`);
|
||||
await animationFrame();
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeChecked();
|
||||
expect(`${cell} .o-checkbox input:only`).not.toBeEnabled();
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input`).toHaveCount(5);
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input:checked`).toHaveCount(3);
|
||||
|
||||
// Fake-check the checkbox
|
||||
await click(cell);
|
||||
await animationFrame();
|
||||
await click(`${cell} .o-checkbox`);
|
||||
await animationFrame();
|
||||
|
||||
await click(`.o_list_button_save`);
|
||||
await animationFrame();
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input`).toHaveCount(5);
|
||||
expect(`tbody td:not(.o_list_record_selector) .o-checkbox input:checked`).toHaveCount(3);
|
||||
});
|
||||
|
||||
test("readonly boolean field", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
expect(`.o_field_boolean input`).not.toBeEnabled();
|
||||
|
||||
await click(`.o_field_boolean .o-checkbox`);
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
expect(`.o_field_boolean input`).not.toBeEnabled();
|
||||
});
|
||||
|
||||
test("onchange return value before toggle checkbox", async () => {
|
||||
Partner._onChanges.bar = (record) => {
|
||||
record["bar"] = true;
|
||||
};
|
||||
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar"/></form>`,
|
||||
});
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
|
||||
await click(`.o_field_boolean .o-checkbox`);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(`.o_field_boolean input`).toBeChecked();
|
||||
});
|
||||
|
|
@ -1,276 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
getFixture,
|
||||
nextTick,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, bar: true },
|
||||
{ id: 2, bar: true },
|
||||
{ id: 3, bar: true },
|
||||
{ id: 4, bar: true },
|
||||
{ id: 5, bar: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BooleanField");
|
||||
|
||||
QUnit.test("boolean field in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="bar" string="Awesome checkbox" />
|
||||
<field name="bar" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:disabled",
|
||||
"checkbox should not be disabled"
|
||||
);
|
||||
|
||||
// uncheck the checkbox
|
||||
await click(target, ".o_field_boolean input:checked");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should no longer be checked"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still no longer be checked"
|
||||
);
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be unchecked"
|
||||
);
|
||||
|
||||
// check the checkbox
|
||||
await click(target, ".o_field_boolean input");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be checked"
|
||||
);
|
||||
|
||||
// uncheck it back
|
||||
await click(target, ".o_field_boolean input");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be unchecked"
|
||||
);
|
||||
|
||||
// check the checkbox by clicking on label
|
||||
await click(target, ".o_form_view label:not(.form-check-label)");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be checked"
|
||||
);
|
||||
|
||||
// uncheck it back
|
||||
await click(target, ".o_form_view label:not(.form-check-label)");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be unchecked"
|
||||
);
|
||||
|
||||
// check the checkbox by hitting the "enter" key after focusing it
|
||||
await triggerEvents(target, ".o_field_boolean input", [
|
||||
["focusin"],
|
||||
["keydown", { key: "Enter" }],
|
||||
["keyup", { key: "Enter" }],
|
||||
]);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should now be checked"
|
||||
);
|
||||
|
||||
// blindly press enter again, it should uncheck the checkbox
|
||||
await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should not be checked"
|
||||
);
|
||||
await nextTick();
|
||||
// blindly press enter again, it should check the checkbox back
|
||||
await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("boolean field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="bar" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input",
|
||||
5,
|
||||
"should have 5 checkboxes"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input:checked",
|
||||
4,
|
||||
"should have 4 checked input"
|
||||
);
|
||||
|
||||
// Edit a line
|
||||
let cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.ok(
|
||||
cell.querySelector(".o-checkbox input:checked").disabled,
|
||||
"input should be disabled in readonly mode"
|
||||
);
|
||||
await click(cell, ".o-checkbox");
|
||||
assert.hasClass(
|
||||
document.querySelector("tr.o_data_row:nth-child(1)"),
|
||||
"o_selected_row",
|
||||
"the row is now selected, in edition"
|
||||
);
|
||||
assert.ok(
|
||||
!cell.querySelector(".o-checkbox input:checked").disabled,
|
||||
"input should now be enabled"
|
||||
);
|
||||
await click(cell);
|
||||
assert.notOk(
|
||||
cell.querySelector(".o-checkbox input:checked").disabled,
|
||||
"input should not have the disabled property in edit mode"
|
||||
);
|
||||
await click(cell, ".o-checkbox");
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.ok(
|
||||
cell.querySelector(".o-checkbox input:not(:checked)").disabled,
|
||||
"input should be disabled again"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox",
|
||||
5,
|
||||
"should still have 5 checkboxes"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input:checked",
|
||||
3,
|
||||
"should now have only 3 checked input"
|
||||
);
|
||||
|
||||
// Re-Edit the line and fake-check the checkbox
|
||||
await click(cell);
|
||||
await click(cell, ".o-checkbox");
|
||||
await click(cell, ".o-checkbox");
|
||||
|
||||
// Save
|
||||
await clickSave(target);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox",
|
||||
5,
|
||||
"should still have 5 checkboxes"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"tbody td:not(.o_list_record_selector) .o-checkbox input:checked",
|
||||
3,
|
||||
"should still have only 3 checked input"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("readonly boolean field", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form><field name="bar" readonly="1"/></form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:disabled",
|
||||
"checkbox should still be disabled"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_boolean .o-checkbox");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should still be checked"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:disabled",
|
||||
"checkbox should still be disabled"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ string: "Bar field" });
|
||||
foo = fields.Boolean();
|
||||
|
||||
_records = [{ id: 1, bar: true, foo: false }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("BooleanIcon field in form view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_icon" options="{'icon': 'fa-recycle'}" />
|
||||
<field name="foo" widget="boolean_icon" options="{'icon': 'fa-trash'}" />
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_boolean_icon button").toHaveCount(2);
|
||||
expect("[name='bar'] button").toHaveAttribute("data-tooltip", "Bar field");
|
||||
expect("[name='bar'] button").toHaveClass("btn-primary fa-recycle");
|
||||
expect("[name='foo'] button").toHaveClass("btn-outline-secondary fa-trash");
|
||||
|
||||
await click("[name='bar'] button");
|
||||
await animationFrame();
|
||||
expect("[name='bar'] button").toHaveClass("btn-outline-secondary fa-recycle");
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { check, click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
bar = fields.Boolean({ default: true });
|
||||
|
||||
_records = [{ id: 1, bar: false }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("use BooleanToggleField in form view", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle"/></form>`,
|
||||
});
|
||||
expect(`.form-check.o_boolean_toggle`).toHaveCount(1);
|
||||
expect(`.o_boolean_toggle input`).toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).not.toBeChecked();
|
||||
|
||||
await check(`.o_field_widget[name='bar'] input`);
|
||||
await animationFrame();
|
||||
expect(`.o_boolean_toggle input`).toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).toBeChecked();
|
||||
});
|
||||
|
||||
test("BooleanToggleField is disabled with a readonly attribute", async () => {
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`.form-check.o_boolean_toggle`).toHaveCount(1);
|
||||
expect(`.o_boolean_toggle input`).not.toBeEnabled();
|
||||
});
|
||||
|
||||
test("BooleanToggleField is disabled if readonly in editable list", async () => {
|
||||
Partner._fields.bar.readonly = true;
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="bar" widget="boolean_toggle"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
expect(`.o_boolean_toggle input`).not.toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).not.toBeChecked();
|
||||
|
||||
await click(`.o_boolean_toggle`);
|
||||
await animationFrame();
|
||||
expect(`.o_boolean_toggle input`).not.toBeEnabled();
|
||||
expect(`.o_boolean_toggle input`).not.toBeChecked();
|
||||
});
|
||||
|
||||
test("BooleanToggleField - auto save record when field toggled", async () => {
|
||||
onRpc("web_save", () => expect.step("web_save"));
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle"/></form>`,
|
||||
});
|
||||
await click(`.o_field_widget[name='bar'] input`);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["web_save"]);
|
||||
});
|
||||
|
||||
test("BooleanToggleField - autosave option set to false", async () => {
|
||||
onRpc("web_save", () => expect.step("web_save"));
|
||||
await mountView({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `<form><field name="bar" widget="boolean_toggle" options="{'autosave': false}"/></form>`,
|
||||
});
|
||||
await click(`.o_field_widget[name='bar'] input`);
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
|
||||
},
|
||||
records: [{ id: 1, bar: false }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BooleanToggleField");
|
||||
|
||||
QUnit.test("use BooleanToggleField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle input:checked",
|
||||
"Boolean toggle should be checked"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle input:not(:checked)",
|
||||
"Boolean toggle shouldn't be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("readonly BooleanToggleField is disabled in edit mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is not disabled in readonly mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable");
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").checked);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is disabled with a readonly attribute", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="bar" widget="boolean_toggle" readonly="1"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is enabled in edit mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").checked);
|
||||
});
|
||||
|
||||
QUnit.test("boolean toggle widget is not disabled in readonly mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:checked");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"boolean toggle widget is disabled with a readonly attribute",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("boolean toggle widget is enabled in edit mode", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".form-check.o_boolean_toggle",
|
||||
"Boolean toggle widget applied to boolean field"
|
||||
);
|
||||
assert.containsNone(target, ".o_boolean_toggle input:checked");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:checked");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"BooleanToggleField is disabled if readonly in editable list",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.bar.readonly = true;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:disabled",
|
||||
"field should be readonly"
|
||||
);
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:not(:checked)");
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:disabled",
|
||||
"field should still be readonly"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:not(:checked)",
|
||||
"should keep unchecked on cell click"
|
||||
);
|
||||
|
||||
await click(target, ".o_boolean_toggle");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_boolean_toggle input:not(:checked)",
|
||||
"should keep unchecked on click"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("BooleanToggleField - auto save record when field toggled", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.verifySteps(["write"]);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField - autosave option set to false", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" options="{'autosave': false}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(_route, { method }) {
|
||||
if (method === "write") {
|
||||
assert.step("write");
|
||||
}
|
||||
},
|
||||
});
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,937 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Currency extends models.Model {
|
||||
digits = fields.Integer();
|
||||
symbol = fields.Char({ string: "Currency Symbol" });
|
||||
position = fields.Char({ string: "Currency Position" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "$",
|
||||
symbol: "$",
|
||||
position: "before",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "€",
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
_inherit = [];
|
||||
|
||||
name = fields.Char({
|
||||
string: "Name",
|
||||
default: "My little Name Value",
|
||||
trim: true,
|
||||
});
|
||||
int_field = fields.Integer();
|
||||
partner_ids = fields.One2many({
|
||||
string: "one2many field",
|
||||
relation: "res.partner",
|
||||
});
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
|
||||
placeholder_name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
name: "yop",
|
||||
int_field: 10,
|
||||
partner_ids: [],
|
||||
placeholder_name: "Placeholder Name",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
name: "blip",
|
||||
int_field: 0,
|
||||
partner_ids: [],
|
||||
},
|
||||
{ id: 3, name: "gnap", int_field: 80 },
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
name: "abc",
|
||||
},
|
||||
{ id: 5, name: "blop", int_field: -4 },
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
color = fields.Integer({ string: "Color index" });
|
||||
name = fields.Char({ string: "Partner Type" });
|
||||
|
||||
_records = [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 37,
|
||||
name: "xphone",
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
name: "xpad",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Users extends models.Model {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Aline",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Christine",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Currency, Partner, PartnerType, Product, Users]);
|
||||
|
||||
test("char field in form view", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_widget input[type='text']").toHaveCount(1, {
|
||||
message: "should have an input for the char field",
|
||||
});
|
||||
expect(".o_field_widget input[type='text']").toHaveValue("yop", {
|
||||
message: "input should contain field value in edit mode",
|
||||
});
|
||||
await fieldInput("name").edit("limbo");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input[type='text']").toHaveValue("limbo", {
|
||||
message: "the new value should be displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("basic rendering text field", async () => {
|
||||
Product._fields.description = fields.Text();
|
||||
Product._records = [{ id: 1, description: "Description as text" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="description" widget="char"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input[type='text']").toHaveCount(1);
|
||||
expect(".o_field_widget input[type='text']").toHaveValue("Description as text");
|
||||
});
|
||||
|
||||
test("setting a char field to empty string is saved as a false value", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].name).toBe(false);
|
||||
});
|
||||
await fieldInput("name").clear();
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("char field with size attribute", async () => {
|
||||
Partner._fields.name.size = 5;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect("input").toHaveAttribute("maxlength", "5", {
|
||||
message: "maxlength attribute should have been set correctly on the input",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("char field in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect("tbody td:not(.o_list_record_selector)").toHaveCount(5, {
|
||||
message: "should have 5 cells",
|
||||
});
|
||||
expect("tbody td:not(.o_list_record_selector):first").toHaveText("yop", {
|
||||
message: "value should be displayed properly as text",
|
||||
});
|
||||
|
||||
const cellSelector = "tbody td:not(.o_list_record_selector)";
|
||||
await contains(cellSelector).click();
|
||||
expect(queryFirst(cellSelector).parentElement).toHaveClass("o_selected_row", {
|
||||
message: "should be set as edit mode",
|
||||
});
|
||||
expect(`${cellSelector} input`).toHaveValue("yop", {
|
||||
message: "should have the corect value in internal input",
|
||||
});
|
||||
await fieldInput("name").edit("brolo", { confirm: false });
|
||||
|
||||
await contains(".o_list_button_save").click();
|
||||
expect(cellSelector).not.toHaveClass("o_selected_row", {
|
||||
message: "should not be in edit mode anymore",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field translatable", async () => {
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
let callGetFieldTranslations = 0;
|
||||
onRpc("res.lang", "get_installed", () => [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
["es_ES", "Spanish"],
|
||||
]);
|
||||
onRpc("res.partner", "get_field_translations", () => {
|
||||
if (callGetFieldTranslations++ === 0) {
|
||||
return [
|
||||
[
|
||||
{ lang: "en_US", source: "yop", value: "yop" },
|
||||
{ lang: "fr_BE", source: "yop", value: "yop français" },
|
||||
{ lang: "es_ES", source: "yop", value: "yop español" },
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: false },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
[
|
||||
{ lang: "en_US", source: "bar", value: "bar" },
|
||||
{ lang: "fr_BE", source: "bar", value: "yop français" },
|
||||
{ lang: "es_ES", source: "bar", value: "bar" },
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: false },
|
||||
];
|
||||
}
|
||||
});
|
||||
onRpc("res.partner", "update_field_translations", function ({ args, kwargs }) {
|
||||
expect(args[2]).toEqual(
|
||||
{ en_US: "bar", es_ES: false },
|
||||
{
|
||||
message:
|
||||
"the new translation value should be written and the value false voids the translation",
|
||||
}
|
||||
);
|
||||
for (const record of this.env["res.partner"].browse(args[0])) {
|
||||
record[args[1]] = args[2][kwargs.context.lang];
|
||||
}
|
||||
return true;
|
||||
});
|
||||
expect("[name=name] input").toHaveClass("o_field_translate");
|
||||
await contains("[name=name] input").click();
|
||||
expect(".o_field_char .btn.o_field_translate").toHaveCount(1, {
|
||||
message: "should have a translate button",
|
||||
});
|
||||
expect(".o_field_char .btn.o_field_translate").toHaveText("EN", {
|
||||
message: "the button should have as test the current language",
|
||||
});
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
expect(".modal").toHaveCount(1, {
|
||||
message: "a translate modal should be visible",
|
||||
});
|
||||
expect(".modal .o_translation_dialog .translation").toHaveCount(3, {
|
||||
message: "three rows should be visible",
|
||||
});
|
||||
let translations = queryAll(".modal .o_translation_dialog .translation input");
|
||||
expect(translations[0]).toHaveValue("yop", {
|
||||
message: "English translation should be filled",
|
||||
});
|
||||
expect(translations[1]).toHaveValue("yop français", {
|
||||
message: "French translation should be filled",
|
||||
});
|
||||
expect(translations[2]).toHaveValue("yop español", {
|
||||
message: "Spanish translation should be filled",
|
||||
});
|
||||
await contains(translations[0]).edit("bar");
|
||||
await contains(translations[2]).clear();
|
||||
await contains("footer .btn.btn-primary").click();
|
||||
expect(".o_field_widget.o_field_char input").toHaveValue("bar", {
|
||||
message: "the new translation should be transfered to modified record",
|
||||
});
|
||||
await fieldInput("name").edit("baz");
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
|
||||
translations = queryAll(".modal .o_translation_dialog .translation input");
|
||||
expect(translations[0]).toHaveValue("baz", {
|
||||
message: "Modified value should be used instead of translation",
|
||||
});
|
||||
expect(translations[1]).toHaveValue("yop français", {
|
||||
message: "French translation shouldn't be changed",
|
||||
});
|
||||
expect(translations[2]).toHaveValue("bar", {
|
||||
message: "Spanish translation should fallback to the English translation",
|
||||
});
|
||||
});
|
||||
|
||||
test("translation dialog should close if field is not there anymore", async () => {
|
||||
expect.assertions(4);
|
||||
// In this test, we simulate the case where the field is removed from the view
|
||||
// this can happen for example if the user click the back button of the browser.
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" />
|
||||
<field name="name" invisible="int_field == 9"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
onRpc("res.lang", "get_installed", () => [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
["es_ES", "Spanish"],
|
||||
]);
|
||||
onRpc("res.partner", "get_field_translations", () => [
|
||||
[
|
||||
{ lang: "en_US", source: "yop", value: "yop" },
|
||||
{ lang: "fr_BE", source: "yop", value: "valeur français" },
|
||||
{ lang: "es_ES", source: "yop", value: "yop español" },
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: false },
|
||||
]);
|
||||
expect("[name=name] input").toHaveClass("o_field_translate");
|
||||
await contains("[name=name] input").click();
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
expect(".modal").toHaveCount(1, {
|
||||
message: "a translate modal should be visible",
|
||||
});
|
||||
await fieldInput("int_field").edit("9");
|
||||
await animationFrame();
|
||||
expect("[name=name] input").toHaveCount(0, {
|
||||
message: "the field name should be invisible",
|
||||
});
|
||||
expect(".modal").toHaveCount(0, {
|
||||
message: "a translate modal should not be visible",
|
||||
});
|
||||
});
|
||||
|
||||
test("html field translatable", async () => {
|
||||
expect.assertions(5);
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
onRpc("res.lang", "get_installed", () => [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
]);
|
||||
onRpc("res.partner", "get_field_translations", () => [
|
||||
[
|
||||
{
|
||||
lang: "en_US",
|
||||
source: "first paragraph",
|
||||
value: "first paragraph",
|
||||
},
|
||||
{
|
||||
lang: "en_US",
|
||||
source: "second paragraph",
|
||||
value: "second paragraph",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "first paragraph",
|
||||
value: "premier paragraphe",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "second paragraph",
|
||||
value: "deuxième paragraphe",
|
||||
},
|
||||
],
|
||||
{
|
||||
translation_type: "char",
|
||||
translation_show_source: true,
|
||||
},
|
||||
]);
|
||||
onRpc("res.partner", "update_field_translations", ({ args }) => {
|
||||
expect(args[2]).toEqual(
|
||||
{ en_US: { "first paragraph": "first paragraph modified" } },
|
||||
{ message: "the new translation value should be written" }
|
||||
);
|
||||
return true;
|
||||
});
|
||||
|
||||
// this will not affect the translate_fields effect until the record is
|
||||
// saved but is set for consistency of the test
|
||||
await fieldInput("name").edit("<p>first paragraph</p><p>second paragraph</p>");
|
||||
await contains(".o_field_char .btn.o_field_translate").click();
|
||||
expect(".modal").toHaveCount(1, {
|
||||
message: "a translate modal should be visible",
|
||||
});
|
||||
expect(".modal .o_translation_dialog .translation").toHaveCount(4, {
|
||||
message: "four rows should be visible",
|
||||
});
|
||||
const enField = queryFirst(".modal .o_translation_dialog .translation input");
|
||||
expect(enField).toHaveValue("first paragraph", {
|
||||
message: "first part of english translation should be filled",
|
||||
});
|
||||
await contains(enField).edit("first paragraph modified");
|
||||
await contains(".modal button.btn-primary").click();
|
||||
expect(".o_field_char input[type='text']").toHaveValue(
|
||||
"<p>first paragraph</p><p>second paragraph</p>",
|
||||
{
|
||||
message: "the new partial translation should not be transfered",
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("char field translatable in create mode", async () => {
|
||||
Partner._fields.name.translate = true;
|
||||
|
||||
serverState.multiLang = true;
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner" });
|
||||
|
||||
expect(".o_field_char .btn.o_field_translate").toHaveCount(1, {
|
||||
message: "should have a translate button in create mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field does not allow html injections", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
await fieldInput("name").edit("<script>throw Error();</script>");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("<script>throw Error();</script>", {
|
||||
message: "the value should have been properly escaped",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field trim (or not) characters", async () => {
|
||||
Partner._fields.foo2 = fields.Char({ trim: false });
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="foo2" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await fieldInput("name").edit(" abc ");
|
||||
await fieldInput("foo2").edit(" def ");
|
||||
await clickSave();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("abc", {
|
||||
message: "Name value should have been trimmed",
|
||||
});
|
||||
expect(".o_field_widget[name='foo2'] input:only").toHaveValue(" def ");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: change value before pending onchange returns", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="product_id" />
|
||||
<field name="name" />
|
||||
</list>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
let def;
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("My little Name Value", {
|
||||
message: "should contain the default value",
|
||||
});
|
||||
|
||||
def = new Deferred();
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
await fieldInput("name").edit("tralala", { confirm: false });
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain tralala",
|
||||
});
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain the same value as before onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: change value before pending onchange returns (2)", async () => {
|
||||
Partner._onChanges.int_field = (obj) => {
|
||||
if (obj.int_field === 7) {
|
||||
obj.name = "blabla";
|
||||
} else {
|
||||
obj.name = "tralala";
|
||||
}
|
||||
};
|
||||
|
||||
const def = new Deferred();
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="int_field" />
|
||||
<field name="name" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop", {
|
||||
message: "should contain the correct value",
|
||||
});
|
||||
|
||||
// trigger a deferred onchange
|
||||
await fieldInput("int_field").edit("7");
|
||||
await fieldInput("name").edit("test", { confirm: false });
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("test", {
|
||||
message: "The onchage value should not be applied because the input is in edition",
|
||||
});
|
||||
await fieldInput("name").press("Enter");
|
||||
await expect(".o_field_widget[name='name'] input").toHaveValue("test");
|
||||
await fieldInput("int_field").edit("10");
|
||||
await expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "The onchange value should be applied because the input is not in edition",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: change value before pending onchange returns (with fieldDebounce)", async () => {
|
||||
// this test is exactly the same as the previous one, except that in
|
||||
// this scenario the onchange return *before* we validate the change
|
||||
// on the input field (before the "change" event is triggered).
|
||||
Partner._onChanges.product_id = (obj) => {
|
||||
obj.int_field = obj.product_id ? 7 : false;
|
||||
};
|
||||
let def;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<field name="int_field"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("My little Name Value", {
|
||||
message: "should contain the default value",
|
||||
});
|
||||
|
||||
def = new Deferred();
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
await fieldInput("name").edit("tralala", { confirm: false });
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain tralala",
|
||||
});
|
||||
expect(".o_field_widget[name='int_field'] input").toHaveValue("");
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain the same value as before onchange",
|
||||
});
|
||||
expect(".o_field_widget[name='int_field'] input").toHaveValue("7", {
|
||||
message: "should contain the value returned by the onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("onchange return value before editing input", async () => {
|
||||
Partner._onChanges.name = (obj) => {
|
||||
obj.name = "yop";
|
||||
};
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop");
|
||||
await fieldInput("name").edit("tralala");
|
||||
await expect("[name='name'] input").toHaveValue("yop");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: change value before pending onchange renaming", async () => {
|
||||
Partner._onChanges.product_id = (obj) => {
|
||||
obj.name = "on change value";
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="product_id" />
|
||||
<field name="name" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
onRpc("onchange", () => def);
|
||||
|
||||
const def = new Deferred();
|
||||
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop", {
|
||||
message: "should contain the correct value",
|
||||
});
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
// set name before onchange
|
||||
await fieldInput("name").edit("tralala");
|
||||
await expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "should contain tralala",
|
||||
});
|
||||
|
||||
// complete the onchange
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("tralala", {
|
||||
message: "input should contain the same value as before onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("support autocomplete attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" autocomplete="coucou"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("autocomplete", "coucou", {
|
||||
message: "attribute autocomplete should be set",
|
||||
});
|
||||
});
|
||||
|
||||
test("input autocomplete attribute set to none by default", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("autocomplete", "off", {
|
||||
message: "attribute autocomplete should be set to none by default",
|
||||
});
|
||||
});
|
||||
|
||||
test("support password attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveValue("yop", {
|
||||
message: "input value should be the password",
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("type", "password", {
|
||||
message: "input should be of type password",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: readonly password", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_char").not.toHaveText("yop", {
|
||||
message: "password field value should be visible in read mode",
|
||||
});
|
||||
|
||||
expect(".o_field_char").toHaveText("***", {
|
||||
message: "password field value should be hidden with '*' in read mode",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: change password value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_char input").toHaveAttribute("type", "password", {
|
||||
message: "password field input value should with type 'password' in edit mode",
|
||||
});
|
||||
expect(".o_field_char input").toHaveValue("yop", {
|
||||
message: "password field input value should be the (hidden) password value",
|
||||
});
|
||||
});
|
||||
|
||||
test("input field: empty password", async () => {
|
||||
Partner._records[0].name = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" password="True"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_char input").toHaveAttribute("type", "password", {
|
||||
message: "password field input value should with type 'password' in edit mode",
|
||||
});
|
||||
expect(".o_field_char input").toHaveValue("", {
|
||||
message: "password field input value should be the (non-hidden, empty) password value",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("input field: set and remove value, then wait for onchange", async () => {
|
||||
Partner._onChanges.product_id = (obj) => {
|
||||
obj.name = obj.product_id ? "onchange value" : false;
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
expect(".o_field_widget[name=name] input").toHaveValue("");
|
||||
await fieldInput("name").edit("test", { confirm: false });
|
||||
await fieldInput("name").clear({ confirm: false });
|
||||
|
||||
// trigger the onchange by setting a product
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--dropdown-item").click();
|
||||
expect(".o_field_widget[name=name] input").toHaveValue("onchange value", {
|
||||
message: "input should contain correct value after onchange",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field with placeholder", async () => {
|
||||
Partner._fields.name.default = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" placeholder="Placeholder" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='name'] input").toHaveAttribute("placeholder", "Placeholder", {
|
||||
message: "placeholder attribute should be set",
|
||||
});
|
||||
});
|
||||
|
||||
test("Form: placeholder_field shows as placeholder", async () => {
|
||||
Partner._records[0].name = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="placeholder_name" invisible="1" />
|
||||
<field name="name" options="{'placeholder_field': 'placeholder_name'}" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect("input").toHaveValue("", {
|
||||
message: "should have no value in input",
|
||||
});
|
||||
expect("input").toHaveAttribute("placeholder", "Placeholder Name", {
|
||||
message: "placeholder_field should be the placeholder",
|
||||
});
|
||||
});
|
||||
|
||||
test("char field: correct value is used to evaluate the modifiers", async () => {
|
||||
Partner._records[0].name = false;
|
||||
Partner._records[0].display_name = false;
|
||||
Partner._onChanges.name = (obj) => {
|
||||
if (obj.name === "a") {
|
||||
obj.display_name = false;
|
||||
} else if (obj.name === "b") {
|
||||
obj.display_name = "";
|
||||
}
|
||||
};
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="name" />
|
||||
<field name="display_name" invisible="'' == display_name"/>
|
||||
</form>`,
|
||||
});
|
||||
expect("[name='display_name']").toHaveCount(1);
|
||||
|
||||
await fieldInput("name").edit("a");
|
||||
await animationFrame();
|
||||
expect("[name='display_name']").toHaveCount(1);
|
||||
|
||||
await fieldInput("name").edit("b");
|
||||
await animationFrame();
|
||||
expect("[name='display_name']").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("edit a char field should display the status indicator buttons without flickering", async () => {
|
||||
Partner._records[0].partner_ids = [2];
|
||||
Partner._onChanges.name = (obj) => {
|
||||
obj.display_name = "cc";
|
||||
};
|
||||
|
||||
const def = new Deferred();
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="partner_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
onRpc("onchange", () => {
|
||||
expect.step("onchange");
|
||||
return def;
|
||||
});
|
||||
expect(".o_form_status_indicator_buttons").not.toBeVisible({
|
||||
message: "form view is not dirty",
|
||||
});
|
||||
await contains(".o_data_cell").click();
|
||||
await fieldInput("name").edit("a");
|
||||
expect(".o_form_status_indicator_buttons").toBeVisible({
|
||||
message: "form view is dirty",
|
||||
});
|
||||
def.resolve();
|
||||
expect.verifySteps(["onchange"]);
|
||||
await animationFrame();
|
||||
expect(".o_form_status_indicator_buttons").toBeVisible({
|
||||
message: "form view is dirty",
|
||||
});
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,139 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Color extends models.Model {
|
||||
hex_color = fields.Char({ string: "hexadecimal color" });
|
||||
text = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hex_color: "#ff4444",
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="hex_color" widget="color" />
|
||||
</group>
|
||||
</form>`,
|
||||
list: /* xml */ `
|
||||
<list editable="bottom">
|
||||
<field name="hex_color" widget="color" />
|
||||
</list>`,
|
||||
};
|
||||
}
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Color, User]);
|
||||
|
||||
test("field contains a color input", async () => {
|
||||
Color._onChanges.hex_color = () => {};
|
||||
await mountView({ type: "form", resModel: "color", resId: 1 });
|
||||
|
||||
onRpc("onchange", ({ args }) => {
|
||||
expect.step(`onchange ${JSON.stringify(args)}`);
|
||||
});
|
||||
|
||||
expect(".o_field_color input[type='color']").toHaveCount(1);
|
||||
|
||||
expect(".o_field_color div").toHaveStyle(
|
||||
{ backgroundColor: "rgba(0, 0, 0, 0)" },
|
||||
{
|
||||
message: "field has the transparent background if no color value has been selected",
|
||||
}
|
||||
);
|
||||
expect(".o_field_color input").toHaveValue("#000000");
|
||||
|
||||
await contains(".o_field_color input", { visible: false }).edit("#fefefe");
|
||||
expect.verifySteps([
|
||||
'onchange [[1],{"hex_color":"#fefefe"},["hex_color"],{"hex_color":{},"display_name":{}}]',
|
||||
]);
|
||||
expect(".o_field_color input").toHaveValue("#fefefe");
|
||||
expect(".o_field_color div").toHaveStyle({ backgroundColor: "rgb(254, 254, 254)" });
|
||||
});
|
||||
|
||||
test("color field in editable list view", async () => {
|
||||
await mountView({ type: "list", resModel: "color", resId: 1 });
|
||||
|
||||
expect(".o_field_color input[type='color']").toHaveCount(2);
|
||||
await contains(".o_field_color input", { visible: false }).click();
|
||||
expect(".o_data_row").not.toHaveClass("o_selected_row");
|
||||
});
|
||||
|
||||
test("read-only color field in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "color",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="hex_color" readonly="1" widget="color" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_field_color input:disabled").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("color field read-only in model definition, in non-editable list", async () => {
|
||||
Color._fields.hex_color.readonly = true;
|
||||
await mountView({ type: "list", resModel: "color" });
|
||||
|
||||
expect(".o_field_color input:disabled").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("color field change via anoter field's onchange", async () => {
|
||||
Color._onChanges.text = (obj) => {
|
||||
obj.hex_color = "#fefefe";
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "color",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="text" />
|
||||
<field name="hex_color" widget="color" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
onRpc("onchange", ({ args }) => {
|
||||
expect.step(`onchange ${JSON.stringify(args)}`);
|
||||
});
|
||||
|
||||
expect(".o_field_color div").toHaveStyle(
|
||||
{ backgroundColor: "rgba(0, 0, 0, 0)" },
|
||||
{
|
||||
message: "field has the transparent background if no color value has been selected",
|
||||
}
|
||||
);
|
||||
expect(".o_field_color input").toHaveValue("#000000");
|
||||
await fieldInput("text").edit("someValue");
|
||||
expect.verifySteps([
|
||||
'onchange [[1],{"text":"someValue"},["text"],{"text":{},"hex_color":{},"display_name":{}}]',
|
||||
]);
|
||||
expect(".o_field_color input").toHaveValue("#fefefe");
|
||||
expect(".o_field_color div").toHaveStyle({ backgroundColor: "rgb(254, 254, 254)" });
|
||||
});
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
import { click, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
hex_color: { string: "hexadecimal color", type: "char" },
|
||||
foo: { type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hex_color: "#ff4444",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("ColorField");
|
||||
|
||||
QUnit.test("field contains a color input", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
hex_color: () => {},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="hex_color" widget="color" />
|
||||
</group>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "onchange") {
|
||||
assert.step(`onchange ${JSON.stringify(args.args)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_color input[type='color']",
|
||||
"native color input is used by the field"
|
||||
);
|
||||
// style returns the value in the rgb format
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"initial",
|
||||
"field has the transparent background if no color value has been selected"
|
||||
);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#000000");
|
||||
await editInput(target, ".o_field_color input", "#fefefe");
|
||||
assert.verifySteps([
|
||||
'onchange [[1],{"id":1,"hex_color":"#fefefe"},"hex_color",{"hex_color":"1"}]',
|
||||
]);
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"rgb(254, 254, 254)",
|
||||
"field has the new color set as background"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="hex_color" widget="color" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_color input[type='color']",
|
||||
2,
|
||||
"native color input is used on each row"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_field_color input"));
|
||||
assert.doesNotHaveClass(target.querySelector(".o_data_row"), "o_selected_row");
|
||||
});
|
||||
|
||||
QUnit.test("read-only color field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="hex_color" readonly="1" widget="color" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
'.o_field_color input:disabled',
|
||||
2,
|
||||
"the field should not be editable"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color field read-only in model definition, in non-editable list", async function (assert) {
|
||||
serverData.models.partner.fields.hex_color.readonly = true;
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="hex_color" widget="color" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
'.o_field_color input:disabled',
|
||||
2,
|
||||
"the field should not be editable"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color field change via another field's onchange", async (assert) => {
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: (rec) => {
|
||||
rec.hex_color = "#fefefe";
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="hex_color" widget="color" />
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "onchange") {
|
||||
assert.step(`onchange ${JSON.stringify(args.args)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"initial",
|
||||
"field has transparent background if no color value has been selected"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#000000");
|
||||
await editInput(target, ".o_field_char[name='foo'] input", "someValue");
|
||||
assert.verifySteps([
|
||||
'onchange [[1],{"id":1,"foo":"someValue","hex_color":false},"foo",{"foo":"1","hex_color":""}]',
|
||||
]);
|
||||
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_color div").style.backgroundColor,
|
||||
"rgb(254, 254, 254)",
|
||||
"field has the new color set as background"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll } from "@odoo/hoot-dom";
|
||||
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
name = fields.Char();
|
||||
int_field = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "partnerName",
|
||||
int_field: 0,
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
list: /* xml */ `
|
||||
<list>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
};
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("No chosen color is a red line with a white background (color 0)", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_color_picker button.o_colorlist_item_color_0").toHaveCount(1);
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect(".o_field_color_picker button.o_colorlist_item_color_0").toHaveCount(1);
|
||||
await contains(".o_field_color_picker .o_colorlist_item_color_3").click();
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect(".o_field_color_picker button.o_colorlist_item_color_0").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("closes when color selected or outside click", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect(queryAll(".o_field_color_picker button").length).toBeGreaterThan(1);
|
||||
await contains(".o_field_color_picker .o_colorlist_item_color_3").click();
|
||||
expect(".o_field_color_picker button").toHaveCount(1);
|
||||
await contains(".o_field_color_picker button").click();
|
||||
await contains(".o_field_widget[name='name'] input").click();
|
||||
expect(".o_field_color_picker button").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("color picker on list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
selectRecord() {
|
||||
expect.step("record selected to open");
|
||||
},
|
||||
});
|
||||
|
||||
await contains(".o_field_color_picker button").click();
|
||||
expect.verifySteps(["record selected to open"]);
|
||||
});
|
||||
|
||||
test("color picker in editable list view", async () => {
|
||||
Partner._records.push({
|
||||
int_field: 1,
|
||||
});
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(1);
|
||||
await contains(".o_data_row:nth-child(1) .o_field_color_picker button").click();
|
||||
expect(".o_data_row:nth-child(1).o_selected_row").toHaveCount(1);
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(12);
|
||||
await contains(
|
||||
".o_data_row:nth-child(1) .o_field_color_picker .o_colorlist_item_color_6"
|
||||
).click();
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(12);
|
||||
await contains(".o_data_row:nth-child(2) .o_data_cell").click();
|
||||
expect(".o_data_row:nth-child(1) .o_field_color_picker button").toHaveCount(1);
|
||||
expect(".o_data_row:nth-child(2) .o_field_color_picker button").toHaveCount(12);
|
||||
});
|
||||
|
||||
test("column widths: dont overflow color picker in list", async () => {
|
||||
Partner._fields.date_field = fields.Date({ string: "Date field" });
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="date_field"/>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</list>`,
|
||||
domain: [["id", "<", 0]],
|
||||
});
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_add", {
|
||||
visible: false,
|
||||
}).click();
|
||||
const date_column_width = queryAll(
|
||||
'.o_list_table thead th[data-name="date_field"]'
|
||||
)[0].style.width.replace("px", "");
|
||||
const int_field_column_width = queryAll(
|
||||
'.o_list_table thead th[data-name="int_field"]'
|
||||
)[0].style.width.replace("px", "");
|
||||
// Default values for date and int fields are: date: '92px', integer: '74px'
|
||||
// With the screen growing, the proportion is kept and thus int_field would remain smaller than date if
|
||||
// the color_picker wouldn't have widthInList set to '1'. With that property set, int_field size will be bigger
|
||||
// than date's one.
|
||||
expect(parseFloat(date_column_width)).toBeLessThan(parseFloat(int_field_column_width), {
|
||||
message: "colorpicker should display properly (Horizontly)",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,243 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: "first",
|
||||
int_field: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("ColorPickerField");
|
||||
|
||||
QUnit.test(
|
||||
"No chosen color is a red line with a white background (color 0)",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_color_picker button"),
|
||||
"o_colorlist_item_color_0",
|
||||
"The default no color value does have the right class"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_color_picker button"),
|
||||
"o_colorlist_item_color_0",
|
||||
"The no color item does have the right class in the list"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker .o_colorlist_item_color_3");
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_color_picker button"),
|
||||
"o_colorlist_item_color_0",
|
||||
"The no color item still have the right class in the list"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("closes when color selected or outside click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_color_picker button").length > 1,
|
||||
true,
|
||||
"there should be more color elements when the component is opened"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker .o_colorlist_item_color_3");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_color_picker button").length,
|
||||
1,
|
||||
"there should be one color element when the component is closed"
|
||||
);
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
|
||||
await click(target.querySelector('.o_field_widget[name="foo"] input'));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_color_picker button").length,
|
||||
1,
|
||||
"there should be one color element when the component is closed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color picker on tree view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
<field name="display_name" />
|
||||
</tree>`,
|
||||
selectRecord() {
|
||||
assert.step("record selected to open");
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_color_picker button");
|
||||
assert.verifySteps(
|
||||
["record selected to open"],
|
||||
"the color is not editable and the record has been opened"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("color picker in editable list view", async function (assert) {
|
||||
serverData.models.partner.records.push({
|
||||
int_field: 1,
|
||||
});
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
"color picker list is not open by default"
|
||||
);
|
||||
|
||||
await click(target, ".o_data_row:nth-child(1) .o_field_color_picker button");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_data_row:nth-child(1)"),
|
||||
"o_selected_row",
|
||||
"first row is selected"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
12,
|
||||
"color picker list is open when the row is in edition"
|
||||
);
|
||||
|
||||
await click(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker .o_colorlist_item_color_6"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
12,
|
||||
"color picker list is still open after color has been selected"
|
||||
);
|
||||
|
||||
await click(target, ".o_data_row:nth-child(2) .o_data_cell");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(1) .o_field_color_picker button",
|
||||
"color picker list is no longer open on the first row"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(2) .o_field_color_picker button",
|
||||
12,
|
||||
"color picker list is open when the row is in edition"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("column widths: dont overflow color picker in list", async function (assert) {
|
||||
serverData.models.partner.fields.date_field = {
|
||||
string: "Date field",
|
||||
type: "date",
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="date_field"/>
|
||||
<field name="int_field" widget="color_picker"/>
|
||||
</tree>`,
|
||||
domain: [["id", "<", 0]],
|
||||
});
|
||||
await click(target.querySelector(".o_list_button_add"));
|
||||
const date_column_width = target
|
||||
.querySelector('.o_list_table thead th[data-name="date_field"]')
|
||||
.style.width.replace("px", "");
|
||||
const int_field_column_width = target
|
||||
.querySelector('.o_list_table thead th[data-name="int_field"]')
|
||||
.style.width.replace("px", "");
|
||||
// Default values for date and int fields are: date: '92px', integer: '74px'
|
||||
// With the screen growing, the proportion is kept and thus int_field would remain smaller than date if
|
||||
// the color_picker wouldn't have widthInList set to '1'. With that property set, int_field size will be bigger
|
||||
// than date's one.
|
||||
assert.ok(
|
||||
parseFloat(date_column_width) < parseFloat(int_field_column_width),
|
||||
"colorpicker should display properly (Horizontly)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
char_field = fields.Char({
|
||||
string: "Char",
|
||||
default: "My little Char Value",
|
||||
trim: true,
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
char_field: "char value",
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardChar"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("Char Field: Copy to clipboard button", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_clipboard_button.o_btn_char_copy").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Show copy button even on empty field", async () => {
|
||||
Partner._records.push({
|
||||
char_field: false,
|
||||
});
|
||||
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 2 });
|
||||
|
||||
expect(".o_field_CopyClipboardChar[name='char_field'] .o_clipboard_button").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Show copy button even on readonly empty field", async () => {
|
||||
Partner._fields.char_field.readonly = true;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardChar" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_CopyClipboardChar[name='char_field'] .o_clipboard_button").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Display a tooltip on click", async () => {
|
||||
mockService("popover", {
|
||||
add(el, comp, params) {
|
||||
expect(params).toEqual({ tooltip: "Copied" });
|
||||
expect.step("copied tooltip");
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
patchWithCleanup(navigator.clipboard, {
|
||||
async writeText(text) {
|
||||
expect.step(text);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await expect(".o_clipboard_button.o_btn_char_copy").toHaveCount(1);
|
||||
await contains(".o_clipboard_button", { visible: false }).click();
|
||||
expect.verifySteps(["char value", "copied tooltip"]);
|
||||
});
|
||||
|
||||
test("CopyClipboardButtonField in form view", async () => {
|
||||
patchWithCleanup(navigator.clipboard, {
|
||||
async writeText(text) {
|
||||
expect.step(text);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardButton"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=char_field] input").toHaveCount(0);
|
||||
expect(".o_clipboard_button.o_btn_char_copy").toHaveCount(1);
|
||||
expect(".o_clipboard_button.o_btn_char_copy").toHaveClass("btn-primary");
|
||||
expect(".o_clipboard_button.o_btn_char_copy").not.toHaveClass("btn-secondary");
|
||||
|
||||
await contains(".o_clipboard_button.o_btn_char_copy").click();
|
||||
|
||||
expect.verifySteps(["char value"]);
|
||||
});
|
||||
|
||||
test("CopyClipboardButtonField with a secondary style", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardButton" options="{'btn_class': 'secondary'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=char_field] input").toHaveCount(0);
|
||||
expect(".o_clipboard_button.o_btn_char_copy").toHaveCount(1);
|
||||
expect(".o_clipboard_button.o_btn_char_copy").not.toHaveClass("btn-primary");
|
||||
expect(".o_clipboard_button.o_btn_char_copy").toHaveClass("btn-secondary");
|
||||
});
|
||||
|
||||
test("CopyClipboardButtonField can be disabled", async () => {
|
||||
patchWithCleanup(navigator.clipboard, {
|
||||
async writeText(text) {
|
||||
expect.step(text);
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" disabled="char_field == 'char value'" widget="CopyClipboardButton"/>
|
||||
<field name="char_field" widget="char"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_clipboard_button.o_btn_char_copy[disabled]").toHaveCount(1);
|
||||
await fieldInput("char_field").edit("another char value");
|
||||
expect(".o_clipboard_button.o_btn_char_copy[disabled]").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { click, getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach((assert) => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
char_field: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
text_field: {
|
||||
string: "txt",
|
||||
type: "text",
|
||||
default: "My little txt Value\nHo-ho-hoooo Merry Christmas",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
char_field: "yop",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("CopyClipboardField");
|
||||
|
||||
QUnit.test("Char & Text Fields: Copy to clipboard button", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardText"/>
|
||||
<field name="char_field" widget="CopyClipboardChar"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_clipboard_button.o_btn_text_copy",
|
||||
"Should have copy button on text type field"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_clipboard_button.o_btn_char_copy",
|
||||
"Should have copy button on char type field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("CopyClipboardField on unset field", async function (assert) {
|
||||
serverData.models.partner.records[0].char_field = false;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="char_field" widget="CopyClipboardChar" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_copy[name="char_field"] .o_clipboard_button',
|
||||
"char_field (unset) should not contain a button"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target.querySelector(".o_field_widget[name=char_field]"),
|
||||
"input",
|
||||
"char_field (unset) should contain an input field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"CopyClipboardField on readonly unset fields in create mode",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.display_name.readonly = true;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name" widget="CopyClipboardChar" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
'.o_field_copy[name="display_name"] .o_clipboard_button',
|
||||
"the readonly unset field should not contain a button"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("CopyClipboard fields: display a tooltip on click", async function (assert) {
|
||||
const fakePopoverService = {
|
||||
async start() {
|
||||
return {
|
||||
add(el, comp, params) {
|
||||
assert.strictEqual(el.textContent, "Copy", "button has the right text");
|
||||
assert.deepEqual(
|
||||
params,
|
||||
{ tooltip: "Copied" },
|
||||
"tooltip has the right parameters"
|
||||
);
|
||||
assert.step("copied tooltip");
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.remove("popover");
|
||||
serviceRegistry.add("popover", fakePopoverService);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
navigator: {
|
||||
clipboard: {
|
||||
writeText: (text) => {
|
||||
assert.strictEqual(
|
||||
text,
|
||||
"My little txt Value\nHo-ho-hoooo Merry Christmas",
|
||||
"copied text is equal to displayed text"
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardText"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_clipboard_button.o_btn_text_copy",
|
||||
"should have copy button on text type field"
|
||||
);
|
||||
|
||||
await click(target, ".o_clipboard_button");
|
||||
await nextTick();
|
||||
assert.verifySteps(["copied tooltip"]);
|
||||
});
|
||||
|
||||
QUnit.test("CopyClipboard fields with clipboard not available", async function (assert) {
|
||||
patchWithCleanup(browser, {
|
||||
console: {
|
||||
warn: (msg) => assert.step(msg),
|
||||
},
|
||||
navigator: {
|
||||
clipboard: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardText"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(target, ".o_clipboard_button");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["This browser doesn't allow to copy to clipboard"],
|
||||
"console simply displays a warning on failure"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("CopyToClipboardButtonField");
|
||||
|
||||
QUnit.test("CopyToClipboardButtonField in form view", async function (assert) {
|
||||
patchWithCleanup(browser, {
|
||||
navigator: {
|
||||
clipboard: {
|
||||
writeText: (text) => {
|
||||
assert.step(text);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<div>
|
||||
<field name="text_field" widget="CopyClipboardButton"/>
|
||||
<field name="char_field" widget="CopyClipboardButton"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsNone(target.querySelector(".o_field_widget[name=char_field]"), "input");
|
||||
assert.containsNone(target.querySelector(".o_field_widget[name=text_field]"), "input");
|
||||
assert.containsOnce(target, ".o_clipboard_button.o_btn_text_copy");
|
||||
assert.containsOnce(target, ".o_clipboard_button.o_btn_char_copy");
|
||||
|
||||
await click(target.querySelector(".o_clipboard_button.o_btn_text_copy"));
|
||||
await click(target.querySelector(".o_clipboard_button.o_btn_char_copy"));
|
||||
|
||||
assert.verifySteps([
|
||||
`My little txt Value
|
||||
Ho-ho-hoooo Merry Christmas`,
|
||||
"yop",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,551 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, press, queryAllTexts, queryOne, scroll } from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockDate, mockTimeZone } from "@odoo/hoot-mock";
|
||||
import {
|
||||
assertDateTimePicker,
|
||||
getPickerCell,
|
||||
zoomOut,
|
||||
} from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "res.partner";
|
||||
|
||||
date = fields.Date();
|
||||
char_field = fields.Char({ string: "Char" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
char_field: "first char field",
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
<field name="char_field"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("toggle datepicker", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
await contains(".o_field_date button").click();
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
await fieldInput("char_field").click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("open datepicker on Control+Enter", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_date input").toHaveCount(1);
|
||||
|
||||
await press(["ctrl", "enter"]);
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
//edit the input and open the datepicker again with ctrl+enter
|
||||
await contains(".o_field_date .o_input").click();
|
||||
await edit("09/01/1997");
|
||||
await press(["ctrl", "enter"]);
|
||||
await animationFrame();
|
||||
assertDateTimePicker({
|
||||
title: "January 1997",
|
||||
date: [
|
||||
{
|
||||
cells: [
|
||||
[29, 30, 31, 1, 2, 3, 4],
|
||||
[5, 6, 7, 8, [9], 10, 11],
|
||||
[12, 13, 14, 15, 16, 17, 18],
|
||||
[19, 20, 21, 22, 23, 24, 25],
|
||||
[26, 27, 28, 29, 30, 31, 1],
|
||||
[2, 3, 4, 5, 6, 7, 8],
|
||||
],
|
||||
daysOfWeek: ["", "S", "M", "T", "W", "T", "F", "S"],
|
||||
weekNumbers: [1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
test("toggle datepicker far in the future", async () => {
|
||||
Partner._records[0].date = "9999-12-31";
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
await contains(".o_field_date button").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// focus another field
|
||||
await fieldInput("char_field").click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("date field is empty if no date is set", async () => {
|
||||
Partner._records[0].date = false;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_date input").toHaveCount(1);
|
||||
expect(".o_field_date input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("set an invalid date when the field is already set", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_widget[name='date'] button").click();
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("02/03/2017");
|
||||
await fieldInput("date").edit("invalid date");
|
||||
await contains(".o_field_widget[name='date'] button").click();
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("02/03/2017", {
|
||||
message: "Should have been reset to the original value",
|
||||
});
|
||||
});
|
||||
|
||||
test("set an invalid date when the field is not set yet", async () => {
|
||||
Partner._records[0].date = false;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
await fieldInput("date").edit("invalid date");
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("value should not set on first click", async () => {
|
||||
Partner._records[0].date = false;
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
await contains(getPickerCell(22)).click();
|
||||
|
||||
await contains(".o_field_date button").click();
|
||||
expect(".o_date_item_cell.o_selected").toHaveText("22");
|
||||
});
|
||||
|
||||
test("date field in form view (with positive time zone offset)", async () => {
|
||||
mockTimeZone(2); // should be ignored by date fields
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(args[1].date);
|
||||
});
|
||||
|
||||
expect(".o_field_date").toHaveText("Feb 3, 2017");
|
||||
|
||||
// open datepicker and select another value
|
||||
await contains(".o_field_date button").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
expect(".o_date_item_cell.o_selected").toHaveCount(1);
|
||||
|
||||
// select 22 Feb 2017
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await contains(getPickerCell("2017")).click();
|
||||
await contains(getPickerCell("Feb")).click();
|
||||
await contains(getPickerCell("22")).click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
expect(".o_field_date").toHaveText("Feb 22, 2017");
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps(["2017-02-22"]);
|
||||
expect(".o_field_date").toHaveText("Feb 22, 2017");
|
||||
});
|
||||
|
||||
test("date field in form view (with negative time zone offset)", async () => {
|
||||
mockTimeZone(-2); // should be ignored by date fields
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_date button").toHaveText("Feb 3, 2017");
|
||||
});
|
||||
|
||||
test("date field dropdown doesn't dissapear on scroll", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<div class="scrollable overflow-auto" style="height: 50px;">
|
||||
<div style="height: 2000px;">
|
||||
<field name="date" />
|
||||
</div>
|
||||
</div>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_date button").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await scroll(".scrollable", { top: 50 });
|
||||
expect(".scrollable").toHaveProperty("scrollTop", 50);
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("date field with label opens datepicker on click", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="date" string="What date is it" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains("label.o_form_label").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("date field with warn_future option ", async () => {
|
||||
Partner._records[0] = { id: 1 };
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" options="{'warn_future': true}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_date input").click();
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await contains(getPickerCell("2020")).click();
|
||||
await contains(getPickerCell("Dec")).click();
|
||||
await contains(getPickerCell("22")).click();
|
||||
expect(".o_field_date button").toHaveClass("text-danger");
|
||||
await contains(".o_field_date button").click();
|
||||
await fieldInput("date").clear();
|
||||
expect(".o_field_date input").not.toHaveClass("text-danger");
|
||||
});
|
||||
|
||||
test("date field with warn_future option: do not overwrite datepicker option", async () => {
|
||||
Partner._onChanges.date = () => {};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
// Do not let the date field get the focus in the first place
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="char_field" />
|
||||
<field name="date" options="{'warn_future': true}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='date']").toHaveText("Feb 3, 2017");
|
||||
await contains(".o_form_button_create").click();
|
||||
expect(".o_field_widget[name='date'] input").toHaveValue("");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("date field in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="date"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const cell = queryOne("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
expect(cell).toHaveText("Feb 3, 2017");
|
||||
await contains(cell).click();
|
||||
expect(".o_field_date button").toHaveCount(1);
|
||||
expect(".o_field_date button").toHaveText("Feb 3, 2017");
|
||||
|
||||
// open datepicker and select another value
|
||||
await contains(".o_field_date button").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await contains(getPickerCell("2017")).click();
|
||||
await contains(getPickerCell("Feb")).click();
|
||||
await contains(getPickerCell("22")).click();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
expect(".o_field_date button").toHaveText("Feb 22, 2017");
|
||||
|
||||
await contains(".o_list_button_save").click();
|
||||
expect("tr.o_data_row td:not(.o_list_record_selector)").toHaveText("Feb 22, 2017");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("multi edition of date field in list view: clear date in input", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
Partner._records = [
|
||||
{ id: 1, date: "2017-02-03" },
|
||||
{ id: 2, date: "2017-02-03" },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="date"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row:eq(0) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row:eq(1) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row:eq(0) .o_data_cell").click();
|
||||
|
||||
expect(".o_field_date button").toHaveCount(1);
|
||||
await contains(".o_field_date button").click();
|
||||
await fieldInput("date").clear();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
await contains(".modal .modal-footer .btn-primary").click();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell").toHaveText("");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell").toHaveText("");
|
||||
});
|
||||
|
||||
test("date field remove value", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect.step(args[1].date);
|
||||
});
|
||||
|
||||
expect(".o_field_date").toHaveText("Feb 3, 2017");
|
||||
|
||||
await contains(".o_field_date button").click();
|
||||
await fieldInput("date").clear();
|
||||
expect(".o_field_date input").toHaveValue("");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_date").toHaveText("");
|
||||
expect.verifySteps([false]);
|
||||
});
|
||||
|
||||
test("date field should select its content onclick when there is one", async () => {
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_date button").click();
|
||||
await contains(".o_field_date input").click();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
const active = document.activeElement;
|
||||
expect(active.tagName).toBe("INPUT");
|
||||
expect(active.value.slice(active.selectionStart, active.selectionEnd)).toBe("02/03/2017");
|
||||
});
|
||||
|
||||
test("date field supports custom formats", async () => {
|
||||
defineParams({ lang_parameters: { date_format: "%d-%m-%Y" } });
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_date").toHaveText("Feb 3, 2017");
|
||||
await contains(".o_field_date button").click();
|
||||
expect(".o_field_date input").toHaveValue("03-02-2017");
|
||||
|
||||
await contains(getPickerCell("22")).click();
|
||||
await clickSave();
|
||||
expect(".o_field_date").toHaveText("Feb 22, 2017");
|
||||
});
|
||||
|
||||
test("date field supports internationalization", async () => {
|
||||
serverState.lang = "nb_NO";
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
|
||||
expect(".o_field_date").toHaveText("3. feb. 2017");
|
||||
await contains(".o_field_date button").click();
|
||||
expect(".o_field_date input").toHaveValue("02/03/2017");
|
||||
expect(".o_zoom_out strong").toHaveText("februar 2017");
|
||||
|
||||
await contains(getPickerCell("22")).click();
|
||||
await clickSave();
|
||||
expect(".o_field_date").toHaveText("22. feb. 2017");
|
||||
});
|
||||
|
||||
test("hit enter should update value", async () => {
|
||||
mockTimeZone(2);
|
||||
await mountView({ type: "form", resModel: "res.partner", resId: 1 });
|
||||
await contains(".o_field_date button").click();
|
||||
await contains(".o_field_date input").edit("01/08");
|
||||
expect(".o_field_widget[name='date']").toHaveText("Jan 8");
|
||||
await contains(".o_field_date button").click();
|
||||
await contains(".o_field_date input").edit("08/01");
|
||||
expect(".o_field_widget[name='date']").toHaveText("Aug 1");
|
||||
});
|
||||
|
||||
test("allow to use compute dates (+5d for instance)", async () => {
|
||||
mockDate({ year: 2021, month: 2, day: 15 });
|
||||
|
||||
Partner._fields.date.default = "2019-09-15";
|
||||
await mountView({ type: "form", resModel: "res.partner" });
|
||||
|
||||
expect(".o_field_date").toHaveText("Sep 15, 2019");
|
||||
await contains(".o_field_date button").click();
|
||||
await fieldInput("date").edit("+5d");
|
||||
expect(".o_field_date").toHaveText("Feb 20");
|
||||
|
||||
// Discard and do it again
|
||||
await contains(".o_form_button_cancel").click();
|
||||
expect(".o_field_date").toHaveText("Sep 15, 2019");
|
||||
await contains(".o_field_date button").click();
|
||||
await fieldInput("date").edit("+5d");
|
||||
expect(".o_field_date").toHaveText("Feb 20");
|
||||
|
||||
// Save and do it again
|
||||
await clickSave();
|
||||
expect(".o_field_date").toHaveText("Feb 20");
|
||||
await contains(".o_field_date button").click();
|
||||
await fieldInput("date").edit("+5d");
|
||||
expect(".o_field_date").toHaveText("Feb 20");
|
||||
});
|
||||
|
||||
test("date field with min_precision option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
// Do not let the date field get the focus in the first place
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="date" options="{'min_precision': 'months'}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(".o_field_date button");
|
||||
await animationFrame();
|
||||
expect(".o_date_item_cell").toHaveCount(12);
|
||||
expect(queryAllTexts(".o_date_item_cell")).toEqual([
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
]);
|
||||
expect(".o_date_item_cell.o_selected").toHaveText("Feb");
|
||||
|
||||
await click(getPickerCell("Jan"));
|
||||
await animationFrame();
|
||||
// The picker should be closed
|
||||
expect(".o_date_item_cell").toHaveCount(0);
|
||||
expect(".o_field_widget[name='date']").toHaveText("Jan 1, 2017");
|
||||
});
|
||||
|
||||
test("date field with max_precision option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
resId: 1,
|
||||
// Do not let the date field get the focus in the first place
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="date" options="{'max_precision': 'months'}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(".o_field_date button");
|
||||
await animationFrame();
|
||||
// Try to zoomOut twice to be in the year selector
|
||||
await zoomOut();
|
||||
// Currently in the month selector
|
||||
expect(".o_datetime_picker_header").toHaveText("2017");
|
||||
await zoomOut();
|
||||
// Stay in the month selector according to the max precision value
|
||||
expect(".o_datetime_picker_header").toHaveText("2017");
|
||||
expect(".o_date_item_cell.o_selected").toHaveText("Feb");
|
||||
|
||||
await click(getPickerCell("Jan"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("12"));
|
||||
await animationFrame();
|
||||
expect(".o_field_widget[name='date']").toHaveText("Jan 12, 2017");
|
||||
});
|
||||
|
||||
test("DateField with onchange forcing a specific date", async () => {
|
||||
mockDate("2009-05-04 10:00:00", +1);
|
||||
|
||||
Partner._onChanges.date = (obj) => {
|
||||
if (obj.char_field === "force today") {
|
||||
obj.date = "2009-05-04";
|
||||
}
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="char_field"/>
|
||||
<field name="date"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_date input").toHaveValue("");
|
||||
|
||||
// enable the onchange
|
||||
await contains(".o_field_widget[name=char_field] input").edit("force today");
|
||||
|
||||
// open the picker and try to set a value different from today
|
||||
await click(".o_field_date input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await contains(getPickerCell("22")).click(); // 22 May 2009
|
||||
expect(".o_field_date").toHaveText("May 4"); // value forced by the onchange
|
||||
|
||||
// do it again (the technical flow is a bit different as now the current value is already today)
|
||||
await click(".o_field_date button");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
await contains(getPickerCell("22")).click(); // 22 May 2009
|
||||
expect(".o_field_date").toHaveText("May 4"); // value forced by the onchange
|
||||
});
|
||||
|
|
@ -1,774 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickCreate,
|
||||
clickDiscard,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchDate,
|
||||
patchTimeZone,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
triggerScroll,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { strftimeToLuxonFormat } from "@web/core/l10n/dates";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
date: { string: "A date", type: "date", searchable: true },
|
||||
datetime: { string: "A datetime", type: "datetime", searchable: true },
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
datetime: "2017-02-08 10:00:00",
|
||||
display_name: "first record",
|
||||
foo: "yop",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
foo: "blip",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
foo: "abc",
|
||||
},
|
||||
{ id: 3, foo: "gnap" },
|
||||
{ id: 5, foo: "blop" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("DateField");
|
||||
|
||||
QUnit.test("DateField: toggle datepicker", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed initially"
|
||||
);
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
// focus another field
|
||||
await click(target, ".o_field_widget[name='foo'] input");
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should close itself when the user clicks outside"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField: toggle datepicker far in the future", async function (assert) {
|
||||
serverData.models.partner.records = [
|
||||
{
|
||||
id: 1,
|
||||
date: "9999-12-30",
|
||||
foo: "yop",
|
||||
},
|
||||
];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed initially"
|
||||
);
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
// focus another field
|
||||
await click(target, ".o_field_widget[name='foo'] input");
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should close itself when the user clicks outside"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("date field is empty if no date is set", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .o_datepicker_input",
|
||||
"should have one input in the form view"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"",
|
||||
"and it should be empty"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DateField: set an invalid date when the field is already set",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
assert.strictEqual(input.value, "02/03/2017");
|
||||
|
||||
input.value = "mmmh";
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(input.value, "02/03/2017", "should have reset the original value");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"DateField: set an invalid date when the field is not set yet",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
assert.strictEqual(input.value, "");
|
||||
|
||||
input.value = "mmmh";
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(input.value, "", "The date field should be empty");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField value should not set on first click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
// open datepicker and select a date
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
"",
|
||||
"date field's input should be empty on first click"
|
||||
);
|
||||
await click(document.body, ".day[data-day*='/22/']");
|
||||
|
||||
// re-open datepicker
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".day.active").textContent,
|
||||
"22",
|
||||
"datepicker should be highlight with 22nd day of month"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField in form view (with positive time zone offset)", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
patchTimeZone(120); // Should be ignored by date fields
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(
|
||||
args[1].date,
|
||||
"2017-02-22",
|
||||
"the correct value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
// open datepicker and select another value
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".day.active[data-day='02/03/2017']",
|
||||
"datepicker should be highlight February 3"
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[1]);
|
||||
await click(document.body.querySelector(".day[data-day*='/22/']"));
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_date input").value,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField in form view (with negative time zone offset)", async function (assert) {
|
||||
patchTimeZone(-120); // Should be ignored by date fields
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField dropdown disappears on scroll", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<div class="scrollable" style="height: 2000px;">
|
||||
<field name="date" />
|
||||
</div>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
|
||||
await triggerScroll(target, { top: 50 });
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField with label opens datepicker on click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="date" string="What date is it" />
|
||||
<field name="date" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector("label.o_form_label"));
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField with warn_future option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="date" options="{ 'datepicker': { 'warn_future': true } }" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// open datepicker and select another value
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[11]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[11]);
|
||||
await click(document.body, ".day[data-day*='/31/']");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_datepicker_warning",
|
||||
"should have a warning in the form view"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "change"); // remove the value
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_datepicker_warning",
|
||||
"the warning in the form view should be hidden"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DateField with warn_future option: do not overwrite datepicker option",
|
||||
async function (assert) {
|
||||
// Making sure we don't have a legit default value
|
||||
// or any onchange that would set the value
|
||||
serverData.models.partner.fields.date.default = undefined;
|
||||
serverData.models.partner.onchanges = {};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" /> <!-- Do not let the date field get the focus in the first place -->
|
||||
<field name="date" options="{ 'datepicker': { 'warn_future': true } }" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
"02/03/2017",
|
||||
"The existing record should have a value for the date field"
|
||||
);
|
||||
|
||||
//Create a new record
|
||||
await clickCreate(target);
|
||||
assert.notOk(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
"The new record should not have a value that the framework would have set"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<tree editable="bottom"><field name="date"/></tree>',
|
||||
});
|
||||
|
||||
const cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.strictEqual(
|
||||
cell.textContent,
|
||||
"02/03/2017",
|
||||
"the date should be displayed correctly in readonly"
|
||||
);
|
||||
await click(cell);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_datepicker_input",
|
||||
"the view should have a date input for editable mode"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input"),
|
||||
document.activeElement,
|
||||
"date input should have the focus"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
// open datepicker and select another value
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[1]);
|
||||
await click(document.body.querySelector(".day[data-day*='/22/']"));
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tr.o_data_row td:not(.o_list_record_selector)").textContent,
|
||||
"02/22/2017",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"multi edition of DateField in list view: clear date in input",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[1].date = "2017-02-03";
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree multi_edit="1"><field name="date"/></tree>',
|
||||
});
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
|
||||
assert.containsOnce(target, "input.o_datepicker_input");
|
||||
await editInput(target, ".o_datepicker_input", "");
|
||||
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
await click(target, ".modal .modal-footer .btn-primary");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField remove value", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(args[1].date, false, "the correct value should be saved");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/03/2017",
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
input.value = "";
|
||||
await triggerEvents(input, null, ["input", "change", "focusout"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"",
|
||||
"should have correctly removed the value"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_date").textContent,
|
||||
"",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"do not trigger a field_changed for datetime field with date widget",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime" widget="date"/></form>',
|
||||
mockRPC(route, { method }) {
|
||||
assert.step(method);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/08/2017",
|
||||
"the date should be correct"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='datetime'] input");
|
||||
input.value = "02/08/2017";
|
||||
await triggerEvents(input, null, ["input", "change", "focusout"]);
|
||||
|
||||
assert.containsOnce(target, ".o_form_saved");
|
||||
assert.verifySteps(["get_views", "read"]); // should not have save as nothing changed
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"field date should select its content onclick when there is one",
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
const done = assert.async();
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
$(target).on("show.datetimepicker", () => {
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"bootstrap-datetimepicker is visible"
|
||||
);
|
||||
const active = document.activeElement;
|
||||
assert.strictEqual(
|
||||
active.tagName,
|
||||
"INPUT",
|
||||
"The datepicker input should be focused"
|
||||
);
|
||||
assert.strictEqual(
|
||||
active.value.slice(active.selectionStart, active.selectionEnd),
|
||||
"02/03/2017",
|
||||
"The whole input of the date field should have been selected"
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateField support internationalization", async function (assert) {
|
||||
// The DatePicker component needs the locale to be available since it
|
||||
// is still using Moment.js for the bootstrap datepicker
|
||||
const originalLocale = moment.locale();
|
||||
moment.defineLocale("no", {
|
||||
monthsShort: "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),
|
||||
monthsParseExact: true,
|
||||
dayOfMonthOrdinalParse: /\d{1,2}\./,
|
||||
ordinal: "%d.",
|
||||
});
|
||||
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({ dateFormat: strftimeToLuxonFormat("%d-%m/%Y") })
|
||||
);
|
||||
patchWithCleanup(luxon.Settings, {
|
||||
defaultLocale: "no",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const dateViewForm = target.querySelector(".o_field_date input").value;
|
||||
await click(target, ".o_datepicker .o_datepicker_input");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
dateViewForm,
|
||||
"input date field should be the same as it was in the view form"
|
||||
);
|
||||
await click(document.body.querySelector(".day[data-day*='/22/']"));
|
||||
const dateEditForm = target.querySelector(".o_datepicker_input").value;
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_date input").value,
|
||||
dateEditForm,
|
||||
"date field should be the same as the one selected in the view form"
|
||||
);
|
||||
|
||||
moment.locale(originalLocale);
|
||||
moment.updateLocale("no", null);
|
||||
});
|
||||
|
||||
QUnit.test("DateField: hit enter should update value", async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="date"/></form>',
|
||||
});
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
const input = target.querySelector(".o_field_widget[name='date'] input");
|
||||
|
||||
input.value = "01/08";
|
||||
await triggerEvent(input, null, "keydown", { key: "Enter" });
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
`01/08/${year}`
|
||||
);
|
||||
|
||||
input.value = "08/01";
|
||||
await triggerEvent(input, null, "keydown", { key: "Enter" });
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='date'] input").value,
|
||||
`08/01/${year}`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("DateField: allow to use compute dates (+5d for instance)", async function (assert) {
|
||||
patchDate(2021, 1, 15, 10, 0, 0); // current date : 15 Feb 2021 10:00:00
|
||||
serverData.models.partner.fields.date.default = "2019-09-15";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="date"></field></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "09/15/2019"); // default date
|
||||
|
||||
// Calculate a new date from current date + 5 days
|
||||
await editInput(target, ".o_field_widget[name=date] .o_datepicker_input", "+5d");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
|
||||
// Discard and do it again
|
||||
await clickDiscard(target);
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "09/15/2019"); // default date
|
||||
await editInput(target, ".o_field_widget[name=date] .o_datepicker_input", "+5d");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
|
||||
// Save and do it again
|
||||
await clickSave(target);
|
||||
// new computed date (current date + 5 days) is saved
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
await editInput(target, ".o_field_widget[name=date] .o_datepicker_input", "+5d");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,690 @@
|
|||
import { after, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
edit,
|
||||
queryAll,
|
||||
queryAllProperties,
|
||||
queryAllTexts,
|
||||
queryRect,
|
||||
resize,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockTimeZone } from "@odoo/hoot-mock";
|
||||
import {
|
||||
editTime,
|
||||
getPickerCell,
|
||||
zoomOut,
|
||||
} from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
contains,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { resetDateFieldWidths } from "@web/views/list/column_width_hook";
|
||||
|
||||
class Partner extends models.Model {
|
||||
date = fields.Date({ string: "A date", searchable: true });
|
||||
datetime = fields.Datetime({ string: "A datetime", searchable: true });
|
||||
p = fields.One2many({
|
||||
string: "one2many field",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
});
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
datetime: "2017-02-08 10:00:00",
|
||||
p: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: false,
|
||||
datetime: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("DatetimeField in form view", async () => {
|
||||
mockTimeZone(+2); // UTC+2
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form>
|
||||
<field name="datetime"/>
|
||||
<field name="datetime" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
|
||||
expect(".o_field_datetime button").toHaveValue(expectedDateString, {
|
||||
message: "the datetime should be correctly displayed",
|
||||
});
|
||||
expect(".o_field_datetime button").toHaveAttribute("data-tooltip", expectedDateString);
|
||||
expect(".o_field_datetime span").toHaveAttribute("data-tooltip", expectedDateString);
|
||||
|
||||
// datepicker should not open on focus
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// select 22 April 2018 at 8:25
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await click(getPickerCell("2018"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("Apr"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("22"));
|
||||
await animationFrame();
|
||||
await editTime("8:25");
|
||||
// Close the datepicker
|
||||
await click(".o_form_view_container");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0, { message: "datepicker should be closed" });
|
||||
|
||||
const newExpectedDateString = "04/22/2018 08:25:00";
|
||||
expect(".o_field_datetime button").toHaveValue(newExpectedDateString, {
|
||||
message: "the selected date should be displayed in the input",
|
||||
});
|
||||
|
||||
// save
|
||||
await clickSave();
|
||||
expect(".o_field_datetime button").toHaveValue(newExpectedDateString, {
|
||||
message: "the selected date should be displayed after saving",
|
||||
});
|
||||
});
|
||||
|
||||
test("DatetimeField only triggers fieldChange when a day is picked and when an hour/minute is selected", async () => {
|
||||
mockTimeZone(+2);
|
||||
|
||||
Partner._onChanges.datetime = () => {};
|
||||
|
||||
onRpc("onchange", () => expect.step("onchange"));
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
// select 22 April 2018 at 8:25
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await click(getPickerCell("2018"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("Apr"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("22"));
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
await editTime("8:25");
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Close the datepicker
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
expect(".o_field_datetime button").toHaveValue("04/22/2018 08:25:00");
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
|
||||
test("DatetimeField edit hour/minute and click away", async () => {
|
||||
mockTimeZone(0);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].datetime).toBe("2017-02-08 08:30:00", {
|
||||
message: "the correct value should be saved",
|
||||
});
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
// Open the datepicker
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// Manually change the time without { confirm: "enter" }
|
||||
await click(`.o_time_picker_input:eq(0)`);
|
||||
await animationFrame();
|
||||
await edit("8:30");
|
||||
await animationFrame();
|
||||
expect(".o_field_datetime input").toHaveValue("02/08/2017 10:00:00", {
|
||||
message: "Input value shouldn't be updated yet",
|
||||
});
|
||||
|
||||
// Close the datepicker
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
expect(".o_field_datetime button").toHaveValue("02/08/2017 08:30:00");
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("DatetimeField with datetime formatted without second", async () => {
|
||||
mockTimeZone(0);
|
||||
|
||||
Partner._fields.datetime = fields.Datetime({
|
||||
string: "A datetime",
|
||||
searchable: true,
|
||||
default: "2017-08-02 12:00:05",
|
||||
required: true,
|
||||
});
|
||||
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%m/%d/%Y",
|
||||
time_format: "%H:%M",
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
const expectedDateString = "08/02/2017 12:00";
|
||||
expect(".o_field_datetime button").toHaveValue(expectedDateString, {
|
||||
message: "the datetime should be correctly displayed",
|
||||
});
|
||||
|
||||
await click(".o_form_button_cancel");
|
||||
expect(".modal").toHaveCount(0, { message: "there should not be a Warning dialog" });
|
||||
});
|
||||
|
||||
test("DatetimeField in editable list view", async () => {
|
||||
mockTimeZone(+2);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="bottom"><field name="datetime"/></list>`,
|
||||
});
|
||||
|
||||
expect("tr.o_data_row td:not(.o_list_record_selector):first").toHaveText(
|
||||
"Feb 8, 2017, 12:00 PM",
|
||||
{
|
||||
message: "the datetime should be correctly displayed",
|
||||
}
|
||||
);
|
||||
|
||||
// switch to edit mode
|
||||
await click(".o_data_row .o_data_cell");
|
||||
await animationFrame();
|
||||
expect(".o_field_datetime button").toHaveCount(1, {
|
||||
message: "the view should have a date input for editable mode",
|
||||
});
|
||||
|
||||
expect(".o_field_datetime button").toBeFocused({
|
||||
message: "date input should have the focus",
|
||||
});
|
||||
|
||||
expect(".o_field_datetime button").toHaveValue("02/08/2017 12:00:00", {
|
||||
message: "the date should be correct in edit mode",
|
||||
});
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// select 22 April 2018 at 8:25
|
||||
await zoomOut();
|
||||
await zoomOut();
|
||||
await click(getPickerCell("2018"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("Apr"));
|
||||
await animationFrame();
|
||||
await click(getPickerCell("22"));
|
||||
await animationFrame();
|
||||
await editTime("8:25");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_datetime input").toHaveValue("04/22/2018 08:25:00", {
|
||||
message: "the date should be correct in edit mode",
|
||||
});
|
||||
// save
|
||||
|
||||
await click(".o_list_button_save");
|
||||
await animationFrame();
|
||||
expect("tr.o_data_row td:not(.o_list_record_selector):first").toHaveText(
|
||||
"Apr 22, 2018, 8:25 AM",
|
||||
{ message: "the selected datetime should be displayed after saving" }
|
||||
);
|
||||
});
|
||||
|
||||
test("DatetimeField input in editable list view keeps its parent's width when empty", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="bottom"><field name="datetime"/></list>`,
|
||||
});
|
||||
await contains(".o_data_row:eq(1) .o_data_cell").click();
|
||||
expect(".o_data_row:eq(1) .o_data_cell input").toHaveRect(
|
||||
queryRect(".o_data_row:eq(1) .o_data_cell .o_field_datetime"),
|
||||
{ message: "input should have the same size as its parent when empty" }
|
||||
);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("multi edition of DatetimeField in list view: edit date in input", async () => {
|
||||
mockTimeZone(0);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list multi_edit="1"><field name="datetime"/></list>',
|
||||
});
|
||||
|
||||
// select two records and edit them
|
||||
await click(".o_data_row:eq(0) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(1) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_data_row:eq(0) .o_data_cell");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_datetime button").toHaveCount(1);
|
||||
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
await edit("10/02/2019 09:00:00", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell:first").toHaveText("Oct 2, 9:00 AM");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell:first").toHaveText("Oct 2, 9:00 AM");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("multi edition of DatetimeField in list view: clear date in input", async () => {
|
||||
Partner._records[1].datetime = "2017-02-08 10:00:00";
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list multi_edit="1"><field name="datetime"/></list>',
|
||||
});
|
||||
|
||||
// select two records and edit them
|
||||
await click(".o_data_row:eq(0) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(1) .o_list_record_selector input");
|
||||
await animationFrame();
|
||||
await click(".o_data_row:eq(0) .o_data_cell");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_datetime button").toHaveCount(1);
|
||||
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
await edit("", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await click(".modal .modal-footer .btn-primary");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row:first-child .o_data_cell:first").toHaveText("");
|
||||
expect(".o_data_row:nth-child(2) .o_data_cell:first").toHaveText("");
|
||||
});
|
||||
|
||||
test("DatetimeField remove value", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
mockTimeZone(+2);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].datetime).toBe(false, { message: "the correct value should be saved" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_datetime button").toHaveValue("02/08/2017 12:00:00", {
|
||||
message: "the date should be correct in edit mode",
|
||||
});
|
||||
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
await edit("");
|
||||
await animationFrame();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_datetime input:first").toHaveValue("", {
|
||||
message: "should have an empty input",
|
||||
});
|
||||
|
||||
// save
|
||||
await clickSave();
|
||||
expect(".o_field_datetime:first").toHaveText("", {
|
||||
message: "the selected date should be displayed after saving",
|
||||
});
|
||||
});
|
||||
|
||||
test("datetime field: hit enter should update value", async () => {
|
||||
// This test verifies that the field datetime is correctly computed when:
|
||||
// - we press enter to validate our entry
|
||||
// - we click outside the field to validate our entry
|
||||
// - we save
|
||||
mockTimeZone(+2);
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Enter a beginning of date and press enter to validate
|
||||
await click(".o_field_datetime button");
|
||||
await animationFrame();
|
||||
await click(".o_field_datetime input");
|
||||
await animationFrame();
|
||||
await edit("01/08/22 14:30", { confirm: "Enter" });
|
||||
|
||||
const datetimeValue = `01/08/2022 14:30:00`;
|
||||
|
||||
expect(".o_field_datetime input:first").toHaveValue(datetimeValue);
|
||||
|
||||
// Click outside the field to check that the field is not changed
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
expect(".o_field_datetime button").toHaveValue(datetimeValue);
|
||||
|
||||
// Save and check that it's still ok
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_datetime button").toHaveValue(datetimeValue);
|
||||
});
|
||||
|
||||
test("DateTimeField with label opens datepicker on click", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<label for="datetime" string="When is it" />
|
||||
<field name="datetime" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click("label.o_form_label");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1, { message: "datepicker should be opened" });
|
||||
});
|
||||
|
||||
test("datetime field: use picker with arabic numbering system", async () => {
|
||||
defineParams({ lang: "ar_001" }); // Select Arab language
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form string="Partners"><field name="datetime" /></form>`,
|
||||
});
|
||||
|
||||
expect("[name=datetime] button").toHaveValue("٠٢/٠٨/٢٠١٧ ١١:٠٠:٠٠");
|
||||
|
||||
await click("[name=datetime] button");
|
||||
await animationFrame();
|
||||
await editTime("11:45");
|
||||
expect("[name=datetime] input").toHaveValue("٠٢/٠٨/٢٠١٧ ١١:٤٥:٠٠");
|
||||
});
|
||||
|
||||
test("datetime field in list view with show_seconds option", async () => {
|
||||
mockTimeZone(+2);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="datetime" widget="datetime" string="show_seconds as false"/>
|
||||
<field name="datetime" widget="datetime" options="{'show_seconds': true}" string="show_seconds as true"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_row:first .o_field_datetime")).toEqual([
|
||||
"Feb 8, 2017, 12:00 PM",
|
||||
"Feb 8, 2017, 12:00:00 PM",
|
||||
]);
|
||||
});
|
||||
|
||||
test("edit a datetime field in form view with show_seconds option", async () => {
|
||||
mockTimeZone(+2);
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="datetime" widget="datetime" string="show_seconds as false"/>
|
||||
<field name="datetime" widget="datetime" options="{'show_seconds': true}" string="show_seconds as true"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_input:eq(0)").click();
|
||||
await animationFrame();
|
||||
expect(".o_time_picker_input").toHaveValue("11:00");
|
||||
await edit("02/08/2017 11:00:00", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_input:eq(0)").toHaveValue("02/08/2017 11:00:00", {
|
||||
message: "seconds should be hidden for showSeconds false",
|
||||
});
|
||||
|
||||
await contains(".o_input:eq(1)").click();
|
||||
await animationFrame();
|
||||
expect(".o_time_picker_input").toHaveValue("11:00:00");
|
||||
await edit("02/08/2017 11:00:30", { confirm: "Enter" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_input:eq(1)").toHaveValue("02/08/2017 11:00:30", {
|
||||
message: "seconds should be visible for showSeconds true",
|
||||
});
|
||||
});
|
||||
|
||||
test("datetime field (with widget) in kanban with show_time option", async () => {
|
||||
mockTimeZone(+2);
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="datetime" widget="datetime" options="{'show_time': false}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_kanban_record:first").toHaveText("Feb 8, 2017");
|
||||
});
|
||||
|
||||
test("datetime field in list with show_time option", async () => {
|
||||
mockTimeZone(+2);
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="datetime" options="{'show_time': false}"/>
|
||||
<field name="datetime" />
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
const dates = queryAll(".o_field_cell");
|
||||
|
||||
expect(dates[0]).toHaveText("Feb 8, 2017", {
|
||||
message: "for date field only date should be visible with date widget",
|
||||
});
|
||||
expect(dates[1]).toHaveText("Feb 8, 2017, 12:00 PM", {
|
||||
message: "for datetime field only date should be visible with date widget",
|
||||
});
|
||||
await contains(dates[0]).click();
|
||||
await animationFrame();
|
||||
expect(".o_field_datetime input:first").toHaveValue("02/08/2017 12:00:00", {
|
||||
message: "for datetime field both date and time should be visible with datetime widget",
|
||||
});
|
||||
});
|
||||
|
||||
test("placeholder_field shows as placeholder (char)", async () => {
|
||||
Partner._fields.char = fields.Char({
|
||||
default: "My Placeholder",
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="char"/>
|
||||
<field name="datetime" options="{'placeholder_field': 'char'}"/>
|
||||
</form>`,
|
||||
});
|
||||
await contains("div[name='datetime'] .o_input").click();
|
||||
expect("div[name='datetime'] .o_input").toHaveAttribute("placeholder", "My Placeholder", {
|
||||
message: "placeholder_field should be the placeholder",
|
||||
});
|
||||
});
|
||||
|
||||
test("placeholder_field shows as placeholder (datetime)", async () => {
|
||||
mockTimeZone(0);
|
||||
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%H:%M",
|
||||
},
|
||||
});
|
||||
|
||||
Partner._fields.datetime_example = fields.Datetime({
|
||||
string: "A datetime",
|
||||
searchable: true,
|
||||
default: "2025-04-01 09:11:11",
|
||||
required: true,
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="datetime_example"/>
|
||||
<field name="datetime" options="{'placeholder_field': 'datetime_example'}"/>
|
||||
</form>`,
|
||||
});
|
||||
await contains("div[name='datetime'] button").click();
|
||||
expect("div[name='datetime'] input").toHaveAttribute("placeholder", "Apr 1, 2025, 9:11 AM", {
|
||||
message: "placeholder_field should be the placeholder",
|
||||
});
|
||||
});
|
||||
|
||||
test("list datetime: column widths (show_time=false)", async () => {
|
||||
await resize({ width: 800 });
|
||||
document.body.style.fontFamily = "sans-serif";
|
||||
resetDateFieldWidths();
|
||||
after(resetDateFieldWidths);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="datetime" widget="datetime" options="{'show_time': false }" />
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_row:eq(0) .o_data_cell")).toEqual(["Feb 8, 2017", "partner,1"]);
|
||||
expect(queryAllProperties(".o_list_table thead th", "offsetWidth")).toEqual([40, 99, 661]);
|
||||
});
|
||||
|
||||
test("list datetime: column widths (numeric format)", async () => {
|
||||
await resize({ width: 800 });
|
||||
document.body.style.fontFamily = "sans-serif";
|
||||
resetDateFieldWidths();
|
||||
after(resetDateFieldWidths);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="datetime" widget="datetime" options="{'numeric': true }" />
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_row:eq(0) .o_data_cell")).toEqual([
|
||||
"02/08/2017 11:00:00",
|
||||
"partner,1",
|
||||
]);
|
||||
expect(queryAllProperties(".o_list_table thead th", "offsetWidth")).toEqual([40, 144, 616]);
|
||||
});
|
||||
|
|
@ -1,661 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchTimeZone,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
date: { string: "A date", type: "date", searchable: true },
|
||||
datetime: { string: "A datetime", type: "datetime", searchable: true },
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
date: "2017-02-03",
|
||||
datetime: "2017-02-08 10:00:00",
|
||||
p: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: false,
|
||||
datetime: false,
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("DatetimeField");
|
||||
|
||||
QUnit.test("DatetimeField in form view", async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime input").value,
|
||||
expectedDateString,
|
||||
"the datetime should be correctly displayed in readonly"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
expectedDateString,
|
||||
"the datetime should be correct in edit mode"
|
||||
);
|
||||
|
||||
// datepicker should not open on focus
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
// select 22 February at 8:25:35
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .day[data-day*='/22/']")
|
||||
);
|
||||
await click(document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o"));
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]);
|
||||
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
|
||||
const newExpectedDateString = "04/22/2017 08:25:35";
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
newExpectedDateString,
|
||||
"the selected date should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime input").value,
|
||||
newExpectedDateString,
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DatetimeField does not trigger fieldChange before datetime completly picked",
|
||||
async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
serverData.models.partner.onchanges = {
|
||||
datetime() {},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "onchange") {
|
||||
assert.step("onchange");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
// select a date and time
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(
|
||||
".bootstrap-datetimepicker-widget .day[data-day*='/22/']"
|
||||
)
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o")
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
|
||||
);
|
||||
assert.verifySteps([], "should not have done any onchange yet");
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]
|
||||
);
|
||||
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"04/22/2017 08:25:35"
|
||||
);
|
||||
assert.verifySteps(["onchange"], "should have done only one onchange");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DatetimeField with datetime formatted without second", async function (assert) {
|
||||
patchTimeZone(0);
|
||||
|
||||
serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
|
||||
serverData.models.partner.fields.datetime.required = true;
|
||||
|
||||
registry.category("services").add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
timeFormat: "HH:mm",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm",
|
||||
}),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
});
|
||||
|
||||
const expectedDateString = "08/02/2017 12:00";
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime input").value,
|
||||
expectedDateString,
|
||||
"the datetime should be correctly displayed in readonly"
|
||||
);
|
||||
|
||||
await click(target, ".o_form_button_cancel");
|
||||
assert.containsNone(document.body, ".modal", "there should not be a Warning dialog");
|
||||
});
|
||||
|
||||
QUnit.test("DatetimeField in editable list view", async function (assert) {
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `<tree editable="bottom"><field name="datetime"/></tree>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
|
||||
const cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
assert.strictEqual(
|
||||
cell.textContent,
|
||||
expectedDateString,
|
||||
"the datetime should be correctly displayed in readonly"
|
||||
);
|
||||
|
||||
// switch to edit mode
|
||||
await click(target.querySelector(".o_data_row .o_data_cell"));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"input.o_datepicker_input",
|
||||
"the view should have a date input for editable mode"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input"),
|
||||
document.activeElement,
|
||||
"date input should have the focus"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_datepicker_input").value,
|
||||
expectedDateString,
|
||||
"the date should be correct in edit mode"
|
||||
);
|
||||
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
await click(target, ".o_datepicker_input");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
// select 22 February at 8:25:35
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
|
||||
);
|
||||
await click(
|
||||
document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .day[data-day*='/22/']")
|
||||
);
|
||||
await click(document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o"));
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]);
|
||||
await click(
|
||||
document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
|
||||
);
|
||||
await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]);
|
||||
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be closed"
|
||||
);
|
||||
|
||||
const newExpectedDateString = "04/22/2017 08:25:35";
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
newExpectedDateString,
|
||||
"the selected datetime should be displayed in the input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tr.o_data_row td:not(.o_list_record_selector)").textContent,
|
||||
newExpectedDateString,
|
||||
"the selected datetime should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"multi edition of DatetimeField in list view: edit date in input",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree multi_edit="1"><field name="datetime"/></tree>',
|
||||
});
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
|
||||
assert.containsOnce(target, "input.o_datepicker_input");
|
||||
await editInput(target, ".o_datepicker_input", "10/02/2019 09:00:00");
|
||||
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
await click(target.querySelector(".modal .modal-footer .btn-primary"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
|
||||
"10/02/2019 09:00:00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
|
||||
"10/02/2019 09:00:00"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"multi edition of DatetimeField in list view: clear date in input",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[1].datetime = "2017-02-08 10:00:00";
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree multi_edit="1"><field name="datetime"/></tree>',
|
||||
});
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
|
||||
// select two records and edit them
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
await click(rows[0], ".o_data_cell");
|
||||
|
||||
assert.containsOnce(target, "input.o_datepicker_input");
|
||||
await editInput(target, ".o_datepicker_input", "");
|
||||
|
||||
assert.containsOnce(document.body, ".modal");
|
||||
await click(target, ".modal .modal-footer .btn-primary");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
|
||||
""
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DatetimeField remove value", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(
|
||||
args[1].datetime,
|
||||
false,
|
||||
"the correct value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"02/08/2017 12:00:00",
|
||||
"the date time should be correct in edit mode"
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
input.value = "";
|
||||
await triggerEvents(input, null, ["input", "change", "focusout"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_datepicker_input").value,
|
||||
"",
|
||||
"should have an empty input"
|
||||
);
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_datetime").textContent,
|
||||
"",
|
||||
"the selected date should be displayed after saving"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"DatetimeField with date/datetime widget (with day change)",
|
||||
async function (assert) {
|
||||
patchTimeZone(-240);
|
||||
|
||||
serverData.models.partner.records[0].p = [2];
|
||||
serverData.models.partner.records[1].datetime = "2017-02-08 02:00:00"; // UTC
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree>
|
||||
<field name="datetime" />
|
||||
</tree>
|
||||
<form>
|
||||
<field name="datetime" widget="date" />
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/07/2017 22:00:00"; // local time zone
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='p'] .o_data_cell").textContent,
|
||||
expectedDateString,
|
||||
"the datetime (datetime widget) should be correctly displayed in tree view"
|
||||
);
|
||||
|
||||
// switch to form view
|
||||
await click(target, ".o_field_widget[name='p'] .o_data_cell");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".modal .o_field_date[name='datetime'] input").value,
|
||||
"02/07/2017",
|
||||
"the datetime (date widget) should be correctly displayed in form view"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"DatetimeField with date/datetime widget (without day change)",
|
||||
async function (assert) {
|
||||
patchTimeZone(-240);
|
||||
|
||||
serverData.models.partner.records[0].p = [2];
|
||||
serverData.models.partner.records[1].datetime = "2017-02-08 10:00:00"; // without timezone
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree>
|
||||
<field name="datetime" />
|
||||
</tree>
|
||||
<form>
|
||||
<field name="datetime" widget="date" />
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const expectedDateString = "02/08/2017 06:00:00"; // with timezone
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='p'] .o_data_cell").textContent,
|
||||
expectedDateString,
|
||||
"the datetime (datetime widget) should be correctly displayed in tree view"
|
||||
);
|
||||
|
||||
// switch to form view
|
||||
await click(target, ".o_field_widget[name='p'] .o_data_cell");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".modal .o_field_date[name='datetime'] input").value,
|
||||
"02/08/2017",
|
||||
"the datetime (date widget) should be correctly displayed in form view"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("datepicker option: daysOfWeekDisabled", async function (assert) {
|
||||
serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
|
||||
serverData.models.partner.fields.datetime.required = true;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="datetime" options="{'datepicker': { 'daysOfWeekDisabled': [0, 6] }}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_datepicker_input");
|
||||
|
||||
for (const el of document.body.querySelectorAll(".day:nth-child(2), .day:last-child")) {
|
||||
assert.hasClass(el, "disabled", "first and last days must be disabled");
|
||||
}
|
||||
|
||||
// the assertions below could be replaced by a single hasClass classic on the jQuery set using the idea
|
||||
// All not <=> not Exists. But we want to be sure that the set is non empty. We don't have an helper
|
||||
// function for that.
|
||||
for (const el of document.body.querySelectorAll(
|
||||
".day:not(:nth-child(2)):not(:last-child)"
|
||||
)) {
|
||||
assert.doesNotHaveClass(el, "disabled", "other days must stay clickable");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("datetime field: hit enter should update value", async function (assert) {
|
||||
// This test verifies that the field datetime is correctly computed when:
|
||||
// - we press enter to validate our entry
|
||||
// - we click outside the field to validate our entry
|
||||
// - we save
|
||||
patchTimeZone(120);
|
||||
|
||||
registry.category("services").add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "%m/%d/%Y",
|
||||
timeFormat: "%H:%M:%S",
|
||||
}),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
const datetime = target.querySelector(".o_field_datetime input");
|
||||
|
||||
// Enter a beginning of date and press enter to validate
|
||||
await editInput(datetime, null, "01/08/22 14:30:40");
|
||||
await triggerEvent(datetime, null, "keydown", { key: "Enter" });
|
||||
|
||||
const datetimeValue = `01/08/2022 14:30:40`;
|
||||
|
||||
assert.strictEqual(datetime.value, datetimeValue);
|
||||
|
||||
// Click outside the field to check that the field is not changed
|
||||
await click(target);
|
||||
assert.strictEqual(datetime.value, datetimeValue);
|
||||
|
||||
// Save and check that it's still ok
|
||||
await clickSave(target);
|
||||
|
||||
const { value } = target.querySelector(".o_field_datetime input");
|
||||
assert.strictEqual(value, datetimeValue);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"datetime field with date widget: hit enter should update value",
|
||||
async function (assert) {
|
||||
/**
|
||||
* Don't think this test is usefull in the new system.
|
||||
*/
|
||||
patchTimeZone(120);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="datetime" widget="date"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget .o_datepicker_input", "01/08/22");
|
||||
await triggerEvent(target, ".o_field_widget .o_datepicker_input", "keydown", {
|
||||
key: "Enter",
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "01/08/2022");
|
||||
|
||||
// Click outside the field to check that the field is not changed
|
||||
await clickSave(target);
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "01/08/2022");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("DateTimeField with label opens datepicker on click", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<label for="datetime" string="When is it" />
|
||||
<field name="datetime" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector("label.o_form_label"));
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".bootstrap-datetimepicker-widget",
|
||||
"datepicker should be opened"
|
||||
);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,174 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { Deferred, press, waitFor, waitUntil } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { DynamicPlaceholderPopover } from "@web/views/fields/dynamic_placeholder_popover";
|
||||
|
||||
class Partner extends models.Model {
|
||||
char = fields.Char();
|
||||
placeholder = fields.Char({ default: "partner" });
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
properties = fields.Properties({
|
||||
string: "Properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "properties_definitions",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
char: "yop",
|
||||
product_id: 37,
|
||||
properties: { f80b6fb58d0d4c72: 3, f424643eee1f3655: 41 },
|
||||
},
|
||||
{ id: 2, char: "blip", product_id: false },
|
||||
{ id: 4, char: "abc", product_id: 41 },
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<field name="placeholder" invisible="1"/>
|
||||
<sheet>
|
||||
<group>
|
||||
<field
|
||||
name="char"
|
||||
options="{
|
||||
'dynamic_placeholder': true,
|
||||
'dynamic_placeholder_model_reference_field': 'placeholder'
|
||||
}"
|
||||
/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
properties_definitions = fields.PropertiesDefinition();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 37,
|
||||
name: "xphone",
|
||||
properties_definitions: [
|
||||
{ name: "f80b6fb58d0d4c72", type: "integer", string: "prop 1" },
|
||||
{ name: "f424643eee1f3655", type: "many2one", string: "prop 2", comodel: "product" },
|
||||
],
|
||||
},
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
onRpc("mail_allowed_qweb_expressions", () => []);
|
||||
|
||||
test("dynamic placeholder close with click out", async () => {
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
await contains(".o_content").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await contains(".o_model_field_selector_popover_item_relation").click();
|
||||
await contains(".o_content").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dynamic placeholder close with escape", async () => {
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
press("Escape");
|
||||
await animationFrame();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await contains(".o_model_field_selector_popover_item_relation").click();
|
||||
press("Escape");
|
||||
await animationFrame();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dynamic placeholder close when clicking on the cross", async () => {
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
await contains(".o_model_field_selector_popover_close").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await contains(".o_model_field_selector_popover_item_relation").click();
|
||||
await contains(".o_model_field_selector_popover_close").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dynamic placeholder properties", async () => {
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
expect(".o_model_field_selector_popover .o_model_field_selector_popover_item_name:contains('Properties')").toHaveCount(1);
|
||||
|
||||
// select the properties
|
||||
await contains(".o_model_field_selector_popover .o_model_field_selector_popover_item_name:contains('Properties') + .o_model_field_selector_popover_item_relation").click();
|
||||
expect(".o_model_field_selector_popover .o_model_field_selector_popover_item_name:contains('prop 1 (xphone)')").toHaveCount(1);
|
||||
expect(".o_model_field_selector_popover .o_model_field_selector_popover_item_name:contains('prop 2 (xphone)')").toHaveCount(1);
|
||||
|
||||
// select the many2one property
|
||||
await contains(".o_model_field_selector_popover .o_model_field_selector_popover_item_name:contains('prop 2 (xphone)') + .o_model_field_selector_popover_item_relation").click();
|
||||
expect(".o_model_field_selector_popover .o_model_field_selector_popover_item_name:contains('Created on')").toHaveCount(1);
|
||||
|
||||
// select the product name
|
||||
await contains(".o_model_field_selector_popover .o_model_field_selector_popover_item_name:contains('Product Name')").click();
|
||||
|
||||
// click on insert
|
||||
await contains(".o_model_field_selector_popover button:contains('Insert')").click();
|
||||
|
||||
const value = document.querySelector(".o_field_placeholder").value.trim();
|
||||
expect(value).toBe("{{object.properties.get('f424643eee1f3655', env['product']).name}}");
|
||||
});
|
||||
|
||||
test("correctly cache model qweb variables and don't prevent opening of other popovers", async () => {
|
||||
const def = new Deferred();
|
||||
let willStarts = 0;
|
||||
patchWithCleanup(DynamicPlaceholderPopover.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onWillStart(() => {
|
||||
willStarts++;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("partner", "mail_allowed_qweb_expressions", async () => {
|
||||
expect.step("mail_allowed_qweb_expressions");
|
||||
await def;
|
||||
return [];
|
||||
});
|
||||
|
||||
await mountView({ type: "form", resModel: "partner", resId: 1 });
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await waitUntil(() => willStarts === 1);
|
||||
await contains(".o_field_char input").edit("#", { confirm: false });
|
||||
await waitUntil(() => willStarts === 2);
|
||||
|
||||
def.resolve();
|
||||
await waitFor(".o_model_field_selector_popover");
|
||||
expect(willStarts).toBe(2);
|
||||
expect.verifySteps(["mail_allowed_qweb_expressions"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "../../web_test_helpers";
|
||||
import { queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
|
||||
class Contact extends models.Model {
|
||||
email = fields.Char();
|
||||
}
|
||||
|
||||
defineModels([Contact]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("in form view", async () => {
|
||||
Contact._records = [{ id: 1, email: "john.doe@odoo.com" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
resId: 1,
|
||||
arch: `<form><field name="email" widget="email"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email input[type="email"]`).toHaveCount(1);
|
||||
expect(`.o_field_email input[type="email"]`).toHaveValue("john.doe@odoo.com");
|
||||
expect(`.o_field_email a`).toHaveCount(1);
|
||||
expect(`.o_field_email a`).toHaveAttribute("href", "mailto:john.doe@odoo.com");
|
||||
expect(`.o_field_email a`).toHaveAttribute("target", "_blank");
|
||||
await fieldInput("email").edit("new@odoo.com");
|
||||
expect(`.o_field_email input[type="email"]`).toHaveValue("new@odoo.com");
|
||||
});
|
||||
|
||||
test("in editable list view", async () => {
|
||||
Contact._records = [
|
||||
{ id: 1, email: "john.doe@odoo.com" },
|
||||
{ id: 2, email: "jane.doe@odoo.com" },
|
||||
];
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "contact",
|
||||
arch: '<list editable="bottom"><field name="email" widget="email"/></list>',
|
||||
});
|
||||
expect(`tbody td:not(.o_list_record_selector) a`).toHaveCount(2);
|
||||
expect(`.o_field_email a`).toHaveCount(2);
|
||||
expect(queryAllTexts(`tbody td:not(.o_list_record_selector) a`)).toEqual([
|
||||
"john.doe@odoo.com",
|
||||
"jane.doe@odoo.com",
|
||||
]);
|
||||
expect(".o_field_email a:first").toHaveAttribute("href", "mailto:john.doe@odoo.com");
|
||||
let cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
await contains(cell).click();
|
||||
expect(cell.parentElement).toHaveClass("o_selected_row");
|
||||
expect(`.o_field_email input[type="email"]`).toHaveValue("john.doe@odoo.com");
|
||||
await fieldInput("email").edit("new@odoo.com");
|
||||
await contains(getFixture()).click();
|
||||
cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
expect(cell.parentElement).not.toHaveClass("o_selected_row");
|
||||
expect(queryAllTexts(`tbody td:not(.o_list_record_selector) a`)).toEqual([
|
||||
"new@odoo.com",
|
||||
"jane.doe@odoo.com",
|
||||
]);
|
||||
expect(".o_field_email a:first").toHaveAttribute("href", "mailto:new@odoo.com");
|
||||
});
|
||||
|
||||
test("with empty value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
arch: `<form><field name="email" widget="email" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email input`).toHaveValue("");
|
||||
});
|
||||
|
||||
test("with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
arch: `<form><field name="email" widget="email" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email input`).toHaveAttribute("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test("placeholder_field shows as placeholder", async () => {
|
||||
Contact._fields.char = fields.Char({
|
||||
default: "My Placeholder",
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
arch: `<form>
|
||||
<field name="email" widget="email" options="{'placeholder_field' : 'char'}"/>
|
||||
<field name="char"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(`.o_field_email input`).toHaveAttribute("placeholder", "My Placeholder");
|
||||
});
|
||||
|
||||
test("trim user value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
arch: '<form><field name="email" widget="email"/></form>',
|
||||
});
|
||||
|
||||
await fieldInput("email").edit(" hello@gmail.com ");
|
||||
await contains(getFixture()).click();
|
||||
expect(`.o_field_email input`).toHaveValue("hello@gmail.com");
|
||||
});
|
||||
|
||||
test("onchange scenario with readonly", async () => {
|
||||
Contact._fields.phone = fields.Char({
|
||||
onChange: (record) => {
|
||||
record.email = "onchange@domain.ext";
|
||||
},
|
||||
});
|
||||
Contact._records = [{ id: 1, email: "default@domain.ext" }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "contact",
|
||||
resId: 1,
|
||||
arch: `<form><field name="phone"/><field name="email" widget="email" readonly="1"/></form>`,
|
||||
});
|
||||
expect(`.o_field_email`).toHaveText("default@domain.ext");
|
||||
await fieldInput("phone").edit("047412345");
|
||||
expect(`.o_field_email`).toHaveText("onchange@domain.ext");
|
||||
});
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
},
|
||||
empty_string: {
|
||||
string: "Empty string",
|
||||
type: "char",
|
||||
default: false,
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [{ foo: "yop" }, { foo: "blip" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("EmailField");
|
||||
|
||||
QUnit.test("EmailField in form view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="email"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// switch to edit mode and check the result
|
||||
const mailtoEdit = target.querySelector('.o_field_email input[type="email"]');
|
||||
assert.containsOnce(target, mailtoEdit, "should have an input for the email field");
|
||||
assert.strictEqual(
|
||||
mailtoEdit.value,
|
||||
"yop",
|
||||
"input should contain field value in edit mode"
|
||||
);
|
||||
|
||||
const emailBtn = target.querySelector(".o_field_email a");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
emailBtn,
|
||||
"should have rendered the email button as a link with correct classes"
|
||||
);
|
||||
assert.hasAttrValue(emailBtn, "href", "mailto:yop", "should have proper mailto prefix");
|
||||
|
||||
// change value in edit mode
|
||||
await editInput(target, ".o_field_email input[type='email']", "new");
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
const mailtoLink = target.querySelector(".o_field_email input[type='email']");
|
||||
assert.strictEqual(mailtoLink.value, "new", "new value should be displayed properly");
|
||||
});
|
||||
|
||||
QUnit.test("EmailField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="email"/></tree>',
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll("tbody td:not(.o_list_record_selector) a").length,
|
||||
2,
|
||||
"should have 2 cells with a link"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tbody td:not(.o_list_record_selector)").textContent,
|
||||
"yop",
|
||||
"value should be displayed properly as text"
|
||||
);
|
||||
|
||||
let mailtoLink = target.querySelectorAll(".o_field_email a");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_email a",
|
||||
2,
|
||||
"should have 2 anchors with correct classes"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
mailtoLink[0],
|
||||
"href",
|
||||
"mailto:yop",
|
||||
"should have proper mailto prefix"
|
||||
);
|
||||
// Edit a line and check the result
|
||||
let cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
assert.hasClass(cell.parentElement, "o_selected_row", "should be set as edit mode");
|
||||
const mailField = cell.querySelector("input");
|
||||
assert.strictEqual(
|
||||
mailField.value,
|
||||
"yop",
|
||||
"should have the correct value in internal input"
|
||||
);
|
||||
await editInput(cell, "input", "new");
|
||||
|
||||
// save
|
||||
await clickSave(target);
|
||||
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
|
||||
assert.doesNotHaveClass(
|
||||
cell.parentElement,
|
||||
"o_selected_row",
|
||||
"should not be in edit mode anymore"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("tbody td:not(.o_list_record_selector)").textContent,
|
||||
"new",
|
||||
"value should be properly updated"
|
||||
);
|
||||
mailtoLink = target.querySelectorAll(".o_field_widget[name='foo'] a");
|
||||
assert.strictEqual(mailtoLink.length, 2, "should still have anchors with correct classes");
|
||||
assert.hasAttrValue(
|
||||
mailtoLink[0],
|
||||
"href",
|
||||
"mailto:new",
|
||||
"should still have proper mailto prefix"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("EmailField with empty value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="empty_string" widget="email" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
const input = target.querySelector(".o_field_email input");
|
||||
assert.strictEqual(input.placeholder, "Placeholder");
|
||||
assert.strictEqual(input.value, "", "the value should be displayed properly");
|
||||
});
|
||||
|
||||
QUnit.test("EmailField trim user value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="foo" widget="email"/></form>',
|
||||
});
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", " abc@abc.com ");
|
||||
const mailFieldInput = target.querySelector('.o_field_widget[name="foo"] input');
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
mailFieldInput.value,
|
||||
"abc@abc.com",
|
||||
"Foo value should have been trimmed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"readonly EmailField is properly rerendered after been changed by onchange",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].foo = "dolores.abernathy@delos";
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" on_change="1"/> <!-- onchange to update mobile in readonly mode directly -->
|
||||
<field name="foo" widget="email" readonly="1"/> <!-- readonly only, we don't want to go through write mode -->
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "onchange") {
|
||||
return Promise.resolve({
|
||||
value: {
|
||||
foo: "lara.espin@unknown", // onchange to update foo in readonly mode directly
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
// check initial rendering
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_email").textContent,
|
||||
"dolores.abernathy@delos",
|
||||
"Initial email text should be set"
|
||||
);
|
||||
|
||||
// edit the phone field, but with the mail in readonly mode
|
||||
await editInput(target, ".o_field_widget[name='int_field'] input", 3);
|
||||
|
||||
// check rendering after changes
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_email").textContent,
|
||||
"lara.espin@unknown",
|
||||
"email text should be updated"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("email field with placeholder", async function (assert) {
|
||||
serverData.models.partner.fields.foo.default = false;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" placeholder="New Placeholder" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='foo'] input").placeholder,
|
||||
"New Placeholder"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { followRelation } from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
|
||||
import { contains, defineModels, fields, models, mountView } from "../../web_test_helpers";
|
||||
|
||||
class Contact extends models.Model {
|
||||
email = fields.Char();
|
||||
child_ids = fields.One2many({ relation: "contact" });
|
||||
}
|
||||
|
||||
class Lead extends models.Model {
|
||||
contact_id = fields.Many2one({ relation: "contact" });
|
||||
salesperson_id = fields.Many2one({ relation: "contact" });
|
||||
note = fields.Text();
|
||||
}
|
||||
|
||||
class UpdateRecordAction extends models.Model {
|
||||
model = fields.Char();
|
||||
update_path = fields.Char();
|
||||
non_searchable = fields.Char({ searchable: false });
|
||||
}
|
||||
|
||||
defineModels([Contact, Lead, UpdateRecordAction]);
|
||||
|
||||
test("readonly", async () => {
|
||||
UpdateRecordAction._records = [
|
||||
{
|
||||
id: 1,
|
||||
update_path: "non_searchable",
|
||||
},
|
||||
];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "update.record.action",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="update_path" widget="field_selector" readonly="1"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_field_widget[name='update_path']").toHaveText("non_searchable");
|
||||
expect(".o_field_widget[name='update_path']").toHaveClass(
|
||||
"o_field_widget o_readonly_modifier o_field_field_selector"
|
||||
);
|
||||
expect(".o_field_widget[name='update_path'] .o_input").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("no specified options", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "update.record.action",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="update_path" widget="field_selector"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await contains(".o_field_widget[name='update_path'] .o_input").click();
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item")).toEqual(
|
||||
[
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Model",
|
||||
"Non searchable",
|
||||
"Update path",
|
||||
],
|
||||
{ message: "should display fields from same model by default" }
|
||||
);
|
||||
});
|
||||
|
||||
test("only_searchable option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "update.record.action",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="update_path" widget="field_selector" options="{'only_searchable': true}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await contains(".o_field_widget[name='update_path'] .o_input").click();
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item")).toEqual(
|
||||
["Created on", "Display name", "Id", "Last Modified on", "Model", "Update path"],
|
||||
{ message: "should not display non searchable fields" }
|
||||
);
|
||||
});
|
||||
|
||||
test("model option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "update.record.action",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="model"/>
|
||||
<field name="update_path" widget="field_selector" options="{'model': 'model'}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await contains(".o_field_widget[name='update_path'] .o_input").click();
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item")).toEqual(
|
||||
[
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Model",
|
||||
"Non searchable",
|
||||
"Update path",
|
||||
],
|
||||
{ message: "should display fields from same model by default" }
|
||||
);
|
||||
await contains(".o_field_widget[name='model'] .o_input").edit("lead");
|
||||
await contains(".o_field_widget[name='update_path'] .o_input").click();
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item")).toEqual(
|
||||
["Contact", "Created on", "Display name", "Id", "Last Modified on", "Note", "Salesperson"],
|
||||
{ message: "should display fields of the specified model" }
|
||||
);
|
||||
expect(".o_model_field_selector_popover_item_relation").toHaveCount(2, {
|
||||
message: "following relations is supported by default",
|
||||
});
|
||||
await followRelation();
|
||||
await animationFrame();
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item")).toEqual(
|
||||
["Childs", "Created on", "Display name", "Email", "Id", "Last Modified on"],
|
||||
{ message: "should display fields of the selected relation" }
|
||||
);
|
||||
});
|
||||
|
||||
test("follow_relations option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "update.record.action",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="model"/>
|
||||
<field name="update_path" widget="field_selector" options="{
|
||||
'model': 'model',
|
||||
'follow_relations': false,
|
||||
}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
await contains(".o_field_widget[name='model'] .o_input").edit("lead");
|
||||
await contains(".o_field_widget[name='update_path'] .o_input").click();
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item")).toEqual(
|
||||
["Contact", "Created on", "Display name", "Id", "Last Modified on", "Note", "Salesperson"],
|
||||
{ message: "should display fields of the specified model" }
|
||||
);
|
||||
expect(".o_model_field_selector_popover_item_relation").toHaveCount(0, {
|
||||
message: "should not allow to follow relations",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
editSelectMenu,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Program extends models.Model {
|
||||
type = fields.Selection({
|
||||
required: true,
|
||||
selection: [
|
||||
["coupon", "Coupons"],
|
||||
["promotion", "Promotion"],
|
||||
["gift_card", "Gift card"],
|
||||
],
|
||||
});
|
||||
available_types = fields.Json({
|
||||
required: true,
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, type: "coupon", available_types: "['coupon', 'promotion']" },
|
||||
{ id: 2, type: "gift_card", available_types: "['gift_card', 'promotion']" },
|
||||
];
|
||||
}
|
||||
defineModels([Program]);
|
||||
|
||||
test(`FilterableSelectionField test whitelist`, async () => {
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="type" widget="filterable_selection" options="{'whitelisted_values': ['coupons', 'promotion']}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
await contains(".o_field_widget[name='type'] input").click();
|
||||
expect(`.o_select_menu_item`).toHaveCount(2);
|
||||
expect(queryAllTexts(".o_select_menu_item")).toEqual(["Coupons", "Promotion"]);
|
||||
});
|
||||
|
||||
test(`FilterableSelectionField test blacklist`, async () => {
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
await contains(".o_field_widget[name='type'] input").click();
|
||||
expect(`.o_select_menu_item`).toHaveCount(2);
|
||||
expect(queryAllTexts(".o_select_menu_item")).toEqual(["Coupons", "Promotion"]);
|
||||
});
|
||||
|
||||
test(`FilterableSelectionField test with invalid value`, async () => {
|
||||
// The field should still display the current value in the list
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 2,
|
||||
});
|
||||
await contains(".o_field_widget[name='type'] input").click();
|
||||
expect(`.o_select_menu_item`).toHaveCount(3);
|
||||
expect(queryAllTexts(".o_select_menu_item")).toEqual(["Coupons", "Promotion", "Gift card"]);
|
||||
await editSelectMenu(".o_field_widget[name='type'] input", { value: "Coupons" });
|
||||
await contains(".o_field_widget[name='type'] input").click();
|
||||
expect(`.o_select_menu_item`).toHaveCount(2);
|
||||
expect(queryAllTexts(".o_select_menu_item")).toEqual(["Coupons", "Promotion"]);
|
||||
});
|
||||
|
||||
test(`FilterableSelectionField test whitelist_fname`, async () => {
|
||||
await mountView({
|
||||
resModel: "program",
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="available_types" invisible="1"/>
|
||||
<field name="type" widget="filterable_selection" options="{'whitelist_fname': 'available_types'}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
await contains(".o_field_widget[name='type'] input").click();
|
||||
expect(`.o_select_menu_item`).toHaveCount(2);
|
||||
expect(queryAllTexts(".o_select_menu_item")).toEqual(["Coupons", "Promotion"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
qux = fields.Float();
|
||||
|
||||
_records = [{ id: 1, qux: 9.1 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("FloatFactorField in form view", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 2.3 / 0.5 = 4.6
|
||||
expect(args[1].qux).toBe(4.6, { message: "the correct float value should be saved" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_factor" options="{'factor': 0.5}" digits="[16,2]" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='qux'] input").toHaveValue("4.55", {
|
||||
message: "The value should be rendered correctly in the input.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='qux'] input").edit("2.3");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("2.30", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("FloatFactorField comma as decimal point", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
// patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "" });
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
decimal_point: ",",
|
||||
thousands_sep: "",
|
||||
},
|
||||
});
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 2.3 / 0.5 = 4.6
|
||||
expect(args[1].qux).toBe(4.6);
|
||||
expect.step("save");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_factor" options="{'factor': 0.5}" digits="[16,2]" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='qux'] input").edit("2,3");
|
||||
await clickSave();
|
||||
|
||||
expect.verifySteps(["save"]);
|
||||
});
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: { qux: { string: "Qux", type: "float", digits: [16, 1] } },
|
||||
records: [{ id: 1, qux: 9.1 }],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FloatFactorField");
|
||||
|
||||
QUnit.test("FloatFactorField in form view", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_factor" options="{'factor': 0.5}" digits="[16,2]" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
// 2.3 / 0.5 = 4.6
|
||||
assert.strictEqual(args[1].qux, 4.6, "the correct float value should be saved");
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='qux'] input").value,
|
||||
"4.55",
|
||||
"The value should be rendered correctly in the input."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='qux'] input", "2.3");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"2.30",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,458 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
class Partner extends models.Model {
|
||||
float_field = fields.Float({ string: "Float field" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, float_field: 0.36 },
|
||||
{ id: 2, float_field: 0 },
|
||||
{ id: 3, float_field: -3.89859 },
|
||||
{ id: 4, float_field: 0 },
|
||||
{ id: 5, float_field: 9.1 },
|
||||
{ id: 100, float_field: 2.034567e3 },
|
||||
{ id: 101, float_field: 3.75675456e6 },
|
||||
{ id: 102, float_field: 6.67543577586e12 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("human readable format 1", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 101,
|
||||
arch: `<form><field name="float_field" options="{'human_readable': 'true'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("4M", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T).",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 2", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 100,
|
||||
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("2.0k", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T).",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 3", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 102,
|
||||
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("6.6754T", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T).",
|
||||
});
|
||||
});
|
||||
|
||||
test("still human readable when readonly", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 102,
|
||||
arch: `<form><field readonly="true" name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget span").toHaveText("6.6754T", {
|
||||
message: "The value should be rendered in human readable format when input is readonly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("unset field should be set to 0", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget").not.toHaveClass("o_field_empty", {
|
||||
message: "Non-set float field should be considered as 0.00",
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("0.00", {
|
||||
message: "Non-set float field should be considered as 0.",
|
||||
});
|
||||
});
|
||||
|
||||
test("use correct digit precision from field definition", async () => {
|
||||
Partner._fields.float_field = fields.Float({ string: "Float field", digits: [0, 1] });
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_float input").toHaveValue("0.4", {
|
||||
message: "should contain a number rounded to 1 decimal",
|
||||
});
|
||||
});
|
||||
|
||||
test("use correct digit precision from options", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 1] }" /></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_float input").toHaveValue("0.4", {
|
||||
message: "should contain a number rounded to 1 decimal",
|
||||
});
|
||||
});
|
||||
|
||||
test("use correct digit precision from field attrs", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field" digits="[0, 1]" /></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_float input").toHaveValue("0.4", {
|
||||
message: "should contain a number rounded to 1 decimal",
|
||||
});
|
||||
});
|
||||
|
||||
test("with 'step' option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{'type': 'number', 'step': 0.3}"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveAttribute("step", "0.3", {
|
||||
message: 'Integer field with option type must have a step attribute equals to "3".',
|
||||
});
|
||||
});
|
||||
|
||||
test("with 'hide_trailing_zeros' option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `<form><field name="float_field" options="{'hide_trailing_zeros': true}"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("9.1", {
|
||||
message: "Input would show 9.10 without the option",
|
||||
});
|
||||
});
|
||||
|
||||
test("basic flow in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget").not.toHaveClass("o_field_empty", {
|
||||
message: "Float field should be considered set for value 0.",
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("0.000", {
|
||||
message: "The value should be displayed properly.",
|
||||
});
|
||||
|
||||
await contains('div[name="float_field"] input').edit("108.2451938598598");
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("108.245", {
|
||||
message: "The value should have been formatted on blur.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("18.8958938598598");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("18.896", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("use a formula", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=20+3*2");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("26.000", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=2**3");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("8.000", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=2^3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("8.000", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=100/3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("33.333", {
|
||||
message: "The new value should be calculated properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("use incorrect formula", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=abc", { confirm: false });
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget[name=float_field]").toHaveClass("o_field_invalid", {
|
||||
message: "fload field should be displayed as invalid",
|
||||
});
|
||||
expect(".o_form_editable").toHaveCount(1, { message: "form view should still be editable" });
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("=3:2?+4", { confirm: false });
|
||||
await clickSave();
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1, { message: "form view should still be editable" });
|
||||
expect(".o_field_widget[name=float_field]").toHaveClass("o_field_invalid", {
|
||||
message: "float field should be displayed as invalid",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("float field in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="float" digits="[5,3]" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
|
||||
expect('div[name="float_field"] input').toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable float.",
|
||||
});
|
||||
|
||||
await contains('div[name="float_field"] input').edit("108.2458938598598", { confirm: "blur" });
|
||||
expect(".o_field_widget:eq(0)").toHaveText("108.246", {
|
||||
message: "The value should have been formatted on blur.",
|
||||
});
|
||||
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
await contains('div[name="float_field"] input').edit("18.8958938598598", { confirm: false });
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
|
||||
expect(".o_field_widget:eq(0)").toHaveText("18.896", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field with type number option", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "number", {
|
||||
message: 'Float field with option type must have a type attribute equals to "number".',
|
||||
});
|
||||
await contains(".o_field_widget input").fill("123456.7890", { instantly: true });
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue(123456.789, {
|
||||
message:
|
||||
"Float value must be not formatted if input type is number. (but the trailing 0 is gone)",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field with type number option and comma decimal separator", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
thousands_sep: ".",
|
||||
decimal_point: ",",
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "number", {
|
||||
message: 'Float field with option type must have a type attribute equals to "number".',
|
||||
});
|
||||
await contains(".o_field_widget[name=float_field] input").fill("123456.789", {
|
||||
instantly: true,
|
||||
});
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue(123456.789, {
|
||||
message: "Float value must be not formatted if input type is number.",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field without type number option", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
resId: 4,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "text", {
|
||||
message: "Float field with option type must have a text type (default type).",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("123456.7890");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("123,456.79", {
|
||||
message: "Float value must be formatted if input type isn't number.",
|
||||
});
|
||||
});
|
||||
|
||||
test("field with enable_formatting option as false", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
grouping: [3, 0],
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{'enable_formatting': false}"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("0.36", {
|
||||
message: "Integer value must not be formatted",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("123456.789");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("123456.789", {
|
||||
message: "Integer value must be not formatted if input type is number.",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("field with enable_formatting option as false in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="float" digits="[5,3]" options="{'enable_formatting': false}" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
|
||||
expect('div[name="float_field"] input').toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable float.",
|
||||
});
|
||||
|
||||
await contains('div[name="float_field"] input').edit("108.2458938598598", {
|
||||
confirm: "blur",
|
||||
});
|
||||
expect(".o_field_widget:eq(0)").toHaveText("108.2458938598598", {
|
||||
message: "The value should not be formatted on blur.",
|
||||
});
|
||||
|
||||
await contains("tr.o_data_row td:not(.o_list_record_selector)").click();
|
||||
await contains('div[name="float_field"] input').edit("18.8958938598598", {
|
||||
confirm: false,
|
||||
});
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
|
||||
expect(".o_field_widget:eq(0)").toHaveText("18.8958938598598", {
|
||||
message: "The new value should not be rounded as well.",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field can be updated by another field/widget", async () => {
|
||||
class MyWidget extends Component {
|
||||
static template = xml`<button t-on-click="onClick">do it</button>`;
|
||||
static props = ["*"];
|
||||
onClick() {
|
||||
const val = this.props.record.data.float_field;
|
||||
this.props.record.update({ float_field: val + 1 });
|
||||
}
|
||||
}
|
||||
const myWidget = {
|
||||
component: MyWidget,
|
||||
};
|
||||
registry.category("view_widgets").add("wi", myWidget);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field"/>
|
||||
<field name="float_field"/>
|
||||
<widget name="wi"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=float_field] input").edit("40");
|
||||
|
||||
expect(".o_field_widget[name=float_field] input:eq(0)").toHaveValue("40.00");
|
||||
expect(".o_field_widget[name=float_field] input:eq(1)").toHaveValue("40.00");
|
||||
|
||||
await contains(".o_widget button").click();
|
||||
|
||||
expect(".o_field_widget[name=float_field] input:eq(0)").toHaveValue("41.00");
|
||||
expect(".o_field_widget[name=float_field] input:eq(1)").toHaveValue("41.00");
|
||||
});
|
||||
|
|
@ -1,479 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, clickSave, editInput, getFixture, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
float_field: { string: "Float field", type: "float" },
|
||||
int_field: { string: "Int field", type: "integer" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, float_field: 0.36 },
|
||||
{ id: 2, float_field: 0 },
|
||||
{ id: 3, float_field: -3.89859 },
|
||||
{ id: 4, float_field: false },
|
||||
{ id: 5, float_field: 9.1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FloatField");
|
||||
|
||||
QUnit.test("unset field should be set to 0", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"Non-set float field should be considered as 0.00"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"0.00",
|
||||
"Non-set float field should be considered as 0."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use correct digit precision from field definition", async function (assert) {
|
||||
serverData.models.partner.fields.float_field.digits = [0, 1];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.4",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use correct digit precision from options", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 1] }" /></form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.4",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use correct digit precision from field attrs", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field" digits="[0, 1]" /></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.4",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with 'step' option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="float_field" options="{'type': 'number', 'step': 0.3}"/></form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("step"),
|
||||
"Integer field with option type must have a step attribute."
|
||||
);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"step",
|
||||
"0.3",
|
||||
'Integer field with option type must have a step attribute equals to "3".'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("basic flow in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"Float field should be considered set for value 0."
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"0.000",
|
||||
"The value should be displayed properly."
|
||||
);
|
||||
|
||||
await editInput(target, 'div[name="float_field"] input', "108.2451938598598");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=float_field] input").value,
|
||||
"108.245",
|
||||
"The value should have been formatted on blur."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "18.8958938598598");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"18.896",
|
||||
"The new value should be rounded properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use a formula", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=20+3*2");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"26.000",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=2**3");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8.000",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=2^3");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8.000",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=100/3");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"33.333",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use incorrect formula", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `<form><field name="float_field" options="{ 'digits': [0, 3] }" /></form>`,
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=abc");
|
||||
await clickSave(target);
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name=float_field]"),
|
||||
"o_field_invalid",
|
||||
"fload field should be displayed as invalid"
|
||||
);
|
||||
assert.containsOnce(target, ".o_form_editable", "form view should still be editable");
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "=3:2?+4");
|
||||
await clickSave(target);
|
||||
|
||||
assert.containsOnce(target, ".o_form_editable", "form view should still be editable");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name=float_field]"),
|
||||
"o_field_invalid",
|
||||
"float field should be displayed as invalid"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="float_field" widget="float" digits="[5,3]" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
// switch to edit mode
|
||||
var cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'div[name="float_field"] input',
|
||||
"The view should have 1 input for editable float."
|
||||
);
|
||||
|
||||
await editInput(target, 'div[name="float_field"] input', "108.2458938598598");
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="float_field"] input').value,
|
||||
"108.246",
|
||||
"The value should have been formatted on blur."
|
||||
);
|
||||
|
||||
await editInput(target, 'div[name="float_field"] input', "18.8958938598598");
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"18.896",
|
||||
"The new value should be rounded properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field with type number option", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({ thousandsSep: ",", grouping: [3, 0] })
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("type"),
|
||||
"Float field with option type must have a type attribute."
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"number",
|
||||
'Float field with option type must have a type attribute equals to "number".'
|
||||
);
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "123456.7890");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"123456.789",
|
||||
"Float value must be not formatted if input type is number. (but the trailing 0 is gone)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"float field with type number option and comma decimal separator",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" options="{'type': 'number'}"/>
|
||||
</form>`,
|
||||
resId: 4,
|
||||
});
|
||||
registry.category("services").remove("localization");
|
||||
registry.category("services").add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
thousandsSep: ".",
|
||||
decimalPoint: ",",
|
||||
grouping: [3, 0],
|
||||
})
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("type"),
|
||||
"Float field with option type must have a type attribute."
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"number",
|
||||
'Float field with option type must have a type attribute equals to "number".'
|
||||
);
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "123456.789");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"123456.789",
|
||||
"Float value must be not formatted if input type is number."
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("float field without type number option", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
resId: 4,
|
||||
});
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({ thousandsSep: ",", grouping: [3, 0] })
|
||||
);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"text",
|
||||
"Float field with option type must have a text type (default type)."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=float_field] input", "123456.7890");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"123,456.79",
|
||||
"Float value must be formatted if input type isn't number."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float_field field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="float_field" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='float_field'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='float_field'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field can be updated by another field/widget", async function (assert) {
|
||||
class MyWidget extends owl.Component {
|
||||
onClick() {
|
||||
const val = this.props.record.data.float_field;
|
||||
this.props.record.update({ float_field: val + 1 });
|
||||
}
|
||||
}
|
||||
MyWidget.template = owl.xml`<button t-on-click="onClick">do it</button>`;
|
||||
registry.category("view_widgets").add("wi", MyWidget);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field"/>
|
||||
<field name="float_field"/>
|
||||
<widget name="wi"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await editInput(
|
||||
target.querySelector(".o_field_widget[name=float_field] input"),
|
||||
null,
|
||||
"40"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
"40.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[0].value
|
||||
);
|
||||
assert.strictEqual(
|
||||
"40.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[1].value
|
||||
);
|
||||
|
||||
await click(target, ".o_widget button");
|
||||
|
||||
assert.strictEqual(
|
||||
"41.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[0].value
|
||||
);
|
||||
assert.strictEqual(
|
||||
"41.00",
|
||||
target.querySelectorAll(".o_field_widget[name=float_field] input")[1].value
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float field with digits=0", async function (assert) {
|
||||
// This isn't in the orm documentation, so it shouldn't be supported, but
|
||||
// people do it and thus now we need to support it.
|
||||
// Historically, it behaves like "no digits attribute defined", so it
|
||||
// fallbacks on a precision of 2 digits.
|
||||
// We will change that in master s.t. we do not round anymore in that case.
|
||||
serverData.models.partner.fields.float_field.digits = 0;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="float_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float input").value,
|
||||
"0.36",
|
||||
"should contain a number rounded to 1 decimal"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
qux = fields.Float();
|
||||
|
||||
_records = [{ id: 5, qux: 9.1 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("FloatTimeField in form view", async () => {
|
||||
expect.assertions(4);
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 48 / 60 = 0.8
|
||||
expect(args[1].qux).toBe(-11.8, {
|
||||
message: "the correct float value should be saved",
|
||||
});
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
// 9 + 0.1 * 60 = 9.06
|
||||
expect(".o_field_float_time[name=qux] input").toHaveValue("09:06", {
|
||||
message: "The value should be rendered correctly in the input.",
|
||||
});
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("-11:48");
|
||||
expect(".o_field_float_time[name=qux] input").toHaveValue("-11:48", {
|
||||
message: "The new value should be displayed properly in the input.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("-11:48", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("FloatTimeField value formatted on blur", async () => {
|
||||
expect.assertions(4);
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
expect(args[1].qux).toBe(9.5, {
|
||||
message: "the correct float value should be saved",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("09:06", {
|
||||
message: "The formatted time value should be displayed properly.",
|
||||
});
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("9.5");
|
||||
expect(".o_field_float_time[name=qux] input").toHaveValue("09:30", {
|
||||
message: "The new value should be displayed properly in the input.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("09:30", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("FloatTimeField with invalid value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("blabla");
|
||||
await clickSave();
|
||||
expect(".o_notification_content").toHaveText("Missing required fields");
|
||||
expect(".o_notification_bar").toHaveClass("bg-danger");
|
||||
expect(".o_field_float_time[name=qux]").toHaveClass("o_field_invalid");
|
||||
|
||||
await contains(".o_field_float_time[name=qux] input").edit("6.5");
|
||||
expect(".o_field_float_time[name=qux] input").not.toHaveClass("o_field_invalid", {
|
||||
message: "date field should not be displayed as invalid now",
|
||||
});
|
||||
});
|
||||
|
||||
test("float_time field does not have an inputmode attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='qux'] input").not.toHaveAttribute("inputmode");
|
||||
});
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
|
||||
},
|
||||
records: [{ id: 5, qux: 9.1 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FloatTimeField");
|
||||
|
||||
QUnit.test("FloatTimeField in form view", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
// 48 / 60 = 0.8
|
||||
assert.strictEqual(
|
||||
args.args[1].qux,
|
||||
-11.8,
|
||||
"the correct float value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
// 9 + 0.1 * 60 = 9.06
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float_time[name=qux] input").value,
|
||||
"09:06",
|
||||
"The value should be rendered correctly in the input."
|
||||
);
|
||||
|
||||
await editInput(
|
||||
target.querySelector(".o_field_float_time[name=qux] input"),
|
||||
null,
|
||||
"-11:48"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float_time[name=qux] input").value,
|
||||
"-11:48",
|
||||
"The new value should be displayed properly in the input."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"-11:48",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FloatTimeField value formatted on blur", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
assert.strictEqual(
|
||||
args.args[1].qux,
|
||||
9.5,
|
||||
"the correct float value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"09:06",
|
||||
"The formatted time value should be displayed properly."
|
||||
);
|
||||
|
||||
await editInput(target.querySelector(".o_field_float_time[name=qux] input"), null, "9.5");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_float_time[name=qux] input").value,
|
||||
"09:30",
|
||||
"The new value should be displayed properly in the input."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"09:30",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("FloatTimeField with invalid value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await editInput(
|
||||
target.querySelector(".o_field_float_time[name=qux] input"),
|
||||
null,
|
||||
"blabla"
|
||||
);
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_title").textContent,
|
||||
"Invalid fields: "
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_content").innerHTML,
|
||||
"<ul><li>Qux</li></ul>"
|
||||
);
|
||||
assert.hasClass(target.querySelector(".o_notification"), "border-danger");
|
||||
assert.hasClass(target.querySelector(".o_field_float_time[name=qux]"), "o_field_invalid");
|
||||
|
||||
await editInput(target.querySelector(".o_field_float_time[name=qux] input"), null, "6.5");
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_float_time[name=qux] input"),
|
||||
"o_field_invalid",
|
||||
"date field should not be displayed as invalid now"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float_time field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='qux'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='qux'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("float_time field does not have an inputmode attribute", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qux" widget="float_time" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='qux'] input");
|
||||
assert.notOk(input.attributes.inputMode);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryText } from "@odoo/hoot-dom";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
float_field = fields.Float({ string: "Float field" });
|
||||
|
||||
_records = [{ id: 1, float_field: 0.44444 }];
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("basic flow in form view", async () => {
|
||||
onRpc("partner", "web_save", ({ args }) => {
|
||||
// 1.000 / 0.125 = 8
|
||||
expect.step(args[1].float_field.toString());
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="float_toggle" options="{'factor': 0.125, 'range': [0, 1, 0.75, 0.5, 0.25]}" digits="[5,3]"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveText("0.056", {
|
||||
message: "The formatted time value should be displayed properly.",
|
||||
});
|
||||
expect("button.o_field_float_toggle").toHaveText("0.056", {
|
||||
message: "The value should be rendered correctly on the button.",
|
||||
});
|
||||
|
||||
await contains("button.o_field_float_toggle").click();
|
||||
|
||||
expect("button.o_field_float_toggle").toHaveText("0.000", {
|
||||
message: "The value should be rendered correctly on the button.",
|
||||
});
|
||||
|
||||
// note, 0 will not be written, it's kept in the _changes of the datapoint.
|
||||
// because save has not been clicked.
|
||||
|
||||
await contains("button.o_field_float_toggle").click();
|
||||
|
||||
expect("button.o_field_float_toggle").toHaveText("1.000", {
|
||||
message: "The value should be rendered correctly on the button.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget").toHaveText("1.000", {
|
||||
message: "The new value should be saved and displayed properly.",
|
||||
});
|
||||
|
||||
expect.verifySteps(["8"]);
|
||||
});
|
||||
|
||||
test("kanban view (readonly) with option force_button", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="float_field" widget="float_toggle" options="{'force_button': true}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect("button.o_field_float_toggle").toHaveCount(1, {
|
||||
message: "should have rendered toggle button",
|
||||
});
|
||||
|
||||
const value = queryText("button.o_field_float_toggle");
|
||||
await contains("button.o_field_float_toggle").click();
|
||||
expect("button.o_field_float_toggle").not.toHaveText(value, {
|
||||
message: "float_field field value should be changed",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
float_field: { string: "Float field", type: "float" },
|
||||
},
|
||||
records: [{ id: 1, float_field: 0.44444 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("FloatToggleField");
|
||||
|
||||
QUnit.test("basic flow in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="float_toggle" options="{'factor': 0.125, 'range': [0, 1, 0.75, 0.5, 0.25]}" digits="[5,3]"/>
|
||||
</form>`,
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/write") {
|
||||
// 1.000 / 0.125 = 8
|
||||
assert.step(args[1].float_field.toString());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"0.056", // 0.4444 * 0.125
|
||||
"The formatted time value should be displayed properly."
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
"0.056",
|
||||
"The value should be rendered correctly on the button."
|
||||
);
|
||||
|
||||
await click(target.querySelector("button.o_field_float_toggle"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
"0.000",
|
||||
"The value should be rendered correctly on the button."
|
||||
);
|
||||
|
||||
// note, 0 will not be written, it's kept in the _changes of the datapoint.
|
||||
// because save has not been clicked.
|
||||
|
||||
await click(target.querySelector("button.o_field_float_toggle"));
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
"1.000",
|
||||
"The value should be rendered correctly on the button."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget").textContent,
|
||||
"1.000",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
|
||||
assert.verifySteps(["8"]);
|
||||
});
|
||||
|
||||
QUnit.test("kanban view (readonly) with option force_button", async function (assert) {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="float_field" widget="float_toggle" options="{'force_button': true}"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"button.o_field_float_toggle",
|
||||
"should have rendered toggle button"
|
||||
);
|
||||
|
||||
const value = target.querySelector("button.o_field_float_toggle").textContent;
|
||||
await click(target.querySelector("button.o_field_float_toggle"));
|
||||
assert.notEqual(
|
||||
target.querySelector("button.o_field_float_toggle").textContent,
|
||||
value,
|
||||
"float_field field value should be changed"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
import { editSelect, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
fonts: {
|
||||
type: "selection",
|
||||
selection: [
|
||||
["Lato", "Lato"],
|
||||
["Oswald", "Oswald"],
|
||||
],
|
||||
default: "Lato",
|
||||
string: "Fonts",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("FontSelectionField");
|
||||
|
||||
QUnit.test("FontSelectionField displays the correct fonts on options", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="fonts" widget="font" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
const options = target.querySelectorAll('.o_field_widget[name="fonts"] option');
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="fonts"] > *').style.fontFamily,
|
||||
"Lato",
|
||||
"Widget font should be default (Lato)"
|
||||
);
|
||||
assert.strictEqual(options[0].value, "false", "Unselected option has no value");
|
||||
assert.strictEqual(
|
||||
options[0].textContent,
|
||||
"Placeholder",
|
||||
"Unselected option is the placeholder"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
options[1].style.fontFamily,
|
||||
"Lato",
|
||||
"Option 1 should have the correct font (Lato)"
|
||||
);
|
||||
assert.strictEqual(
|
||||
options[2].style.fontFamily,
|
||||
"Oswald",
|
||||
"Option 2 should have the correct font (Oswald)"
|
||||
);
|
||||
|
||||
await editSelect(target, ".o_input", options[2].value);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="fonts"] > *').style.fontFamily,
|
||||
"Oswald",
|
||||
"Widget font should be updated (Oswald)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"FontSelectionField displays one blank option (not required)",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.fonts.selection = [
|
||||
[false, ""],
|
||||
...serverData.models.partner.fields.fonts.selection,
|
||||
];
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="fonts" widget="font"/></form>',
|
||||
});
|
||||
assert.containsN(target.querySelector(".o_field_widget[name='fonts']"), "option", 3);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { allowTranslations, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { markup } from "@odoo/owl";
|
||||
import { currencies } from "@web/core/currency";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import {
|
||||
formatFloat,
|
||||
formatFloatFactor,
|
||||
formatFloatTime,
|
||||
formatJson,
|
||||
formatInteger,
|
||||
formatMany2one,
|
||||
formatMany2oneReference,
|
||||
formatMonetary,
|
||||
formatPercentage,
|
||||
formatReference,
|
||||
formatText,
|
||||
formatX2many,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
} from "@web/views/fields/formatters";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
beforeEach(() => {
|
||||
allowTranslations();
|
||||
patchWithCleanup(localization, {
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
decimalPoint: ".",
|
||||
thousandsSep: ",",
|
||||
grouping: [3, 0],
|
||||
});
|
||||
});
|
||||
|
||||
test("formatFloat", () => {
|
||||
expect(formatFloat(false)).toBe("");
|
||||
expect(formatFloat(200)).toBe("200.00");
|
||||
expect(formatFloat(200, { trailingZeros: false })).toBe("200");
|
||||
});
|
||||
|
||||
test("formatFloatFactor", () => {
|
||||
expect(formatFloatFactor(false)).toBe("");
|
||||
expect(formatFloatFactor(6000)).toBe("6,000.00");
|
||||
expect(formatFloatFactor(6000, { factor: 3 })).toBe("18,000.00");
|
||||
expect(formatFloatFactor(6000, { factor: 0.5 })).toBe("3,000.00");
|
||||
});
|
||||
|
||||
test("formatFloatTime", () => {
|
||||
expect(formatFloatTime(2)).toBe("02:00");
|
||||
expect(formatFloatTime(3.5)).toBe("03:30");
|
||||
expect(formatFloatTime(0.25)).toBe("00:15");
|
||||
expect(formatFloatTime(0.58)).toBe("00:35");
|
||||
expect(formatFloatTime(2 / 60, { displaySeconds: true })).toBe("00:02:00");
|
||||
expect(formatFloatTime(2 / 60 + 1 / 3600, { displaySeconds: true })).toBe("00:02:01");
|
||||
expect(formatFloatTime(2 / 60 + 2 / 3600, { displaySeconds: true })).toBe("00:02:02");
|
||||
expect(formatFloatTime(2 / 60 + 3 / 3600, { displaySeconds: true })).toBe("00:02:03");
|
||||
expect(formatFloatTime(0.25, { displaySeconds: true })).toBe("00:15:00");
|
||||
expect(formatFloatTime(0.25 + 15 / 3600, { displaySeconds: true })).toBe("00:15:15");
|
||||
expect(formatFloatTime(0.25 + 45 / 3600, { displaySeconds: true })).toBe("00:15:45");
|
||||
expect(formatFloatTime(56 / 3600, { displaySeconds: true })).toBe("00:00:56");
|
||||
expect(formatFloatTime(-0.5)).toBe("-00:30");
|
||||
|
||||
const options = { noLeadingZeroHour: true };
|
||||
expect(formatFloatTime(2, options)).toBe("2:00");
|
||||
expect(formatFloatTime(3.5, options)).toBe("3:30");
|
||||
expect(formatFloatTime(3.5, { ...options, displaySeconds: true })).toBe("3:30:00");
|
||||
expect(formatFloatTime(3.5 + 15 / 3600, { ...options, displaySeconds: true })).toBe("3:30:15");
|
||||
expect(formatFloatTime(3.5 + 45 / 3600, { ...options, displaySeconds: true })).toBe("3:30:45");
|
||||
expect(formatFloatTime(56 / 3600, { ...options, displaySeconds: true })).toBe("0:00:56");
|
||||
expect(formatFloatTime(-0.5, options)).toBe("-0:30");
|
||||
});
|
||||
|
||||
test("formatJson", () => {
|
||||
expect(formatJson(false)).toBe("");
|
||||
expect(formatJson({})).toBe("{}");
|
||||
expect(formatJson({ 1: 111 })).toBe('{"1":111}');
|
||||
expect(formatJson({ 9: 11, 666: 42 })).toBe('{"9":11,"666":42}');
|
||||
});
|
||||
|
||||
test("formatInteger", () => {
|
||||
expect(formatInteger(false)).toBe("");
|
||||
expect(formatInteger(0)).toBe("0");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
expect(formatInteger(1000000)).toBe("1,000,000");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
expect(formatInteger(106500)).toBe("1,06,500");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
expect(formatInteger(106500)).toBe("106,50,0");
|
||||
|
||||
const options = { grouping: [2, 0], thousandsSep: "€" };
|
||||
expect(formatInteger(6000, options)).toBe("60€00");
|
||||
});
|
||||
|
||||
test("formatMany2one", () => {
|
||||
expect(formatMany2one(false)).toBe("");
|
||||
expect(formatMany2one([false, "M2O value"])).toBe("M2O value");
|
||||
expect(formatMany2one([1, false])).toBe("Unnamed");
|
||||
expect(formatMany2one([1, "M2O value"])).toBe("M2O value");
|
||||
expect(formatMany2one([1, "M2O value"], { escape: true })).toBe("M2O%20value");
|
||||
expect(formatMany2one({ id: false, display_name: "M2O value" })).toBe("M2O value");
|
||||
expect(formatMany2one({ id: 1, display_name: false })).toBe("Unnamed");
|
||||
expect(formatMany2one({ id: 1, display_name: "M2O value" })).toBe("M2O value");
|
||||
expect(formatMany2one({ id: 1, display_name: "M2O value" }, { escape: true })).toBe(
|
||||
"M2O%20value"
|
||||
);
|
||||
});
|
||||
|
||||
test("formatText", () => {
|
||||
expect(formatText(false)).toBe("");
|
||||
expect(formatText("value")).toBe("value");
|
||||
expect(formatText(1)).toBe("1");
|
||||
expect(formatText(1.5)).toBe("1.5");
|
||||
expect(formatText(markup`<p>This is a Test</p>`)).toBe("<p>This is a Test</p>");
|
||||
expect(formatText([1, 2, 3, 4, 5])).toBe("1,2,3,4,5");
|
||||
expect(formatText({ a: 1, b: 2 })).toBe("[object Object]");
|
||||
});
|
||||
|
||||
test("formatX2many", () => {
|
||||
// Results are cast as strings since they're lazy translated.
|
||||
expect(String(formatX2many({ currentIds: [] }))).toBe("No records");
|
||||
expect(String(formatX2many({ currentIds: [1] }))).toBe("1 record");
|
||||
expect(String(formatX2many({ currentIds: [1, 3] }))).toBe("2 records");
|
||||
});
|
||||
|
||||
test("formatMonetary", () => {
|
||||
patchWithCleanup(currencies, {
|
||||
10: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
11: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
12: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "&",
|
||||
},
|
||||
});
|
||||
|
||||
expect(formatMonetary(false)).toBe("");
|
||||
|
||||
const field = {
|
||||
type: "monetary",
|
||||
currency_field: "c_x",
|
||||
};
|
||||
let data = {
|
||||
c_x: [11],
|
||||
c_y: 12,
|
||||
};
|
||||
expect(formatMonetary(200, { field, currencyId: 10, data })).toBe("200.00\u00a0€");
|
||||
expect(formatMonetary(200, { field, currencyId: 10, data, trailingZeros: false })).toBe(
|
||||
"200\u00a0€"
|
||||
);
|
||||
expect(formatMonetary(200, { field, data })).toBe("$\u00a0200.00");
|
||||
expect(formatMonetary(200, { field, currencyField: "c_y", data })).toBe("200.00\u00a0&");
|
||||
|
||||
const floatField = { type: "float" };
|
||||
data = {
|
||||
currency_id: [11],
|
||||
};
|
||||
expect(formatMonetary(200, { field: floatField, data })).toBe("$\u00a0200.00");
|
||||
});
|
||||
|
||||
test("formatPercentage", () => {
|
||||
expect(formatPercentage(false)).toBe("0%");
|
||||
expect(formatPercentage(0)).toBe("0%");
|
||||
expect(formatPercentage(0.5)).toBe("50%");
|
||||
|
||||
expect(formatPercentage(1)).toBe("100%");
|
||||
|
||||
expect(formatPercentage(-0.2)).toBe("-20%");
|
||||
expect(formatPercentage(2.5)).toBe("250%");
|
||||
|
||||
expect(formatPercentage(0.125)).toBe("12.5%");
|
||||
expect(formatPercentage(0.666666)).toBe("66.67%");
|
||||
expect(formatPercentage(125)).toBe("12500%");
|
||||
|
||||
expect(formatPercentage(50, { humanReadable: true })).toBe("5k%");
|
||||
expect(formatPercentage(0.5, { noSymbol: true })).toBe("50");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 0], decimalPoint: ",", thousandsSep: "." });
|
||||
expect(formatPercentage(0.125)).toBe("12,5%");
|
||||
expect(formatPercentage(0.666666)).toBe("66,67%");
|
||||
});
|
||||
|
||||
test("formatReference", () => {
|
||||
expect(formatReference(false)).toBe("");
|
||||
const value = { resModel: "product", resId: 2, displayName: "Chair" };
|
||||
expect(formatReference(value)).toBe("Chair");
|
||||
});
|
||||
|
||||
test("formatMany2oneReference", () => {
|
||||
expect(formatMany2oneReference(false)).toBe("");
|
||||
expect(formatMany2oneReference({ resId: 9, displayName: "Chair" })).toBe("Chair");
|
||||
});
|
||||
|
||||
test("formatDate", () => {
|
||||
expect(formatDate(false)).toBe("");
|
||||
expect(formatDate(DateTime.fromObject({ day: 22, month: 1, year: 1990 }))).toBe("Jan 22, 1990");
|
||||
expect(
|
||||
formatDate(DateTime.fromObject({ day: 22, month: 1, year: 1990 }), { numeric: true })
|
||||
).toBe("01/22/1990");
|
||||
expect(formatDate(DateTime.fromObject({ day: 22, month: 1 }))).toBe("Jan 22");
|
||||
});
|
||||
|
||||
test("formatDateTime", () => {
|
||||
const datetime = DateTime.fromObject({
|
||||
day: 22,
|
||||
month: 1,
|
||||
year: 1990,
|
||||
hour: 10,
|
||||
minute: 30,
|
||||
second: 45,
|
||||
});
|
||||
expect(formatDateTime(false)).toBe("");
|
||||
expect(formatDateTime(datetime)).toBe("Jan 22, 1990, 10:30 AM");
|
||||
expect(formatDateTime(datetime, { showDate: false })).toBe("10:30 AM");
|
||||
expect(formatDateTime(datetime, { showSeconds: true })).toBe("Jan 22, 1990, 10:30:45 AM");
|
||||
expect(formatDateTime(datetime, { showTime: false })).toBe("Jan 22, 1990");
|
||||
expect(formatDateTime(datetime, { numeric: true })).toBe("01/22/1990 10:30:45");
|
||||
expect(formatDateTime(DateTime.fromObject({ day: 22, month: 1, hour: 10, minute: 30 }))).toBe(
|
||||
"Jan 22, 10:30 AM"
|
||||
);
|
||||
});
|
||||
|
|
@ -1,358 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { session } from "@web/session";
|
||||
import {
|
||||
formatFloat,
|
||||
formatFloatFactor,
|
||||
formatFloatTime,
|
||||
formatJson,
|
||||
formatInteger,
|
||||
formatMany2one,
|
||||
formatMonetary,
|
||||
formatPercentage,
|
||||
formatReference,
|
||||
formatX2many,
|
||||
} from "@web/views/fields/formatters";
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
});
|
||||
|
||||
QUnit.module("Formatters");
|
||||
|
||||
QUnit.test("formatFloat", function (assert) {
|
||||
assert.strictEqual(formatFloat(false), "");
|
||||
assert.strictEqual(formatFloat(null), "0.00");
|
||||
assert.strictEqual(formatFloat(1000000), "1,000,000.00");
|
||||
|
||||
const options = { grouping: [3, 2, -1], decimalPoint: "?", thousandsSep: "€" };
|
||||
assert.strictEqual(formatFloat(106500, options), "1€06€500?00");
|
||||
|
||||
assert.strictEqual(formatFloat(1500, { thousandsSep: "" }), "1500.00");
|
||||
assert.strictEqual(formatFloat(-1.01), "-1.01");
|
||||
assert.strictEqual(formatFloat(-0.01), "-0.01");
|
||||
|
||||
assert.strictEqual(formatFloat(38.0001, { noTrailingZeros: true }), "38");
|
||||
assert.strictEqual(formatFloat(38.1, { noTrailingZeros: true }), "38.1");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
assert.strictEqual(formatFloat(1000000), "1,000,000.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
assert.strictEqual(formatFloat(106500), "1,06,500.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
assert.strictEqual(formatFloat(106500), "106,50,0.00");
|
||||
|
||||
patchWithCleanup(localization, {
|
||||
grouping: [2, 0],
|
||||
decimalPoint: "!",
|
||||
thousandsSep: "@",
|
||||
});
|
||||
assert.strictEqual(formatFloat(6000), "60@00!00");
|
||||
});
|
||||
|
||||
QUnit.test("formatFloat (humanReadable=true)", async (assert) => {
|
||||
assert.strictEqual(
|
||||
formatFloat(1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1020000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"1,020k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(10200000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"10.20M"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1002, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.00k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(101, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"101.00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(64.2, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"64.20"
|
||||
);
|
||||
assert.strictEqual(formatFloat(1e18, { humanReadable: true }), "1E");
|
||||
assert.strictEqual(
|
||||
formatFloat(1e21, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1e+21"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.0045e22, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.0045e22, { humanReadable: true, decimals: 3, minDigits: 1 }),
|
||||
"1.005e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.012e43, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.012e43, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-1,020k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-10200000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-10.20M"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1002, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.00k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-101, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-101.00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-64.2, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-64.20"
|
||||
);
|
||||
assert.strictEqual(formatFloat(-1e18, { humanReadable: true }), "-1E");
|
||||
assert.strictEqual(
|
||||
formatFloat(-1e21, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1e+21"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.0045e22, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.0045e22, { humanReadable: true, decimals: 3, minDigits: 1 }),
|
||||
"-1.004e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.012e43, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.012e43, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-1.01e+43"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("formatFloatFactor", function (assert) {
|
||||
assert.strictEqual(formatFloatFactor(false), "");
|
||||
assert.strictEqual(formatFloatFactor(6000), "6,000.00");
|
||||
assert.strictEqual(formatFloatFactor(6000, { factor: 3 }), "18,000.00");
|
||||
assert.strictEqual(formatFloatFactor(6000, { factor: 0.5 }), "3,000.00");
|
||||
});
|
||||
|
||||
QUnit.test("formatFloatTime", function (assert) {
|
||||
assert.strictEqual(formatFloatTime(2), "02:00");
|
||||
assert.strictEqual(formatFloatTime(3.5), "03:30");
|
||||
assert.strictEqual(formatFloatTime(0.25), "00:15");
|
||||
assert.strictEqual(formatFloatTime(0.58), "00:35");
|
||||
assert.strictEqual(formatFloatTime(2 / 60, { displaySeconds: true }), "00:02:00");
|
||||
assert.strictEqual(
|
||||
formatFloatTime(2 / 60 + 1 / 3600, { displaySeconds: true }),
|
||||
"00:02:01"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(2 / 60 + 2 / 3600, { displaySeconds: true }),
|
||||
"00:02:02"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(2 / 60 + 3 / 3600, { displaySeconds: true }),
|
||||
"00:02:03"
|
||||
);
|
||||
assert.strictEqual(formatFloatTime(0.25, { displaySeconds: true }), "00:15:00");
|
||||
assert.strictEqual(formatFloatTime(0.25 + 15 / 3600, { displaySeconds: true }), "00:15:15");
|
||||
assert.strictEqual(formatFloatTime(0.25 + 45 / 3600, { displaySeconds: true }), "00:15:45");
|
||||
assert.strictEqual(formatFloatTime(56 / 3600, { displaySeconds: true }), "00:00:56");
|
||||
assert.strictEqual(formatFloatTime(-0.5), "-00:30");
|
||||
|
||||
const options = { noLeadingZeroHour: true };
|
||||
assert.strictEqual(formatFloatTime(2, options), "2:00");
|
||||
assert.strictEqual(formatFloatTime(3.5, options), "3:30");
|
||||
assert.strictEqual(formatFloatTime(3.5, { ...options, displaySeconds: true }), "3:30:00");
|
||||
assert.strictEqual(
|
||||
formatFloatTime(3.5 + 15 / 3600, { ...options, displaySeconds: true }),
|
||||
"3:30:15"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(3.5 + 45 / 3600, { ...options, displaySeconds: true }),
|
||||
"3:30:45"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloatTime(56 / 3600, { ...options, displaySeconds: true }),
|
||||
"0:00:56"
|
||||
);
|
||||
assert.strictEqual(formatFloatTime(-0.5, options), "-0:30");
|
||||
});
|
||||
|
||||
QUnit.test("formatJson", function (assert) {
|
||||
assert.strictEqual(formatJson(false), "");
|
||||
assert.strictEqual(formatJson({}), "{}");
|
||||
assert.strictEqual(formatJson({ 1: 111 }), '{"1":111}');
|
||||
assert.strictEqual(formatJson({ 9: 11, 666: 42 }), '{"9":11,"666":42}');
|
||||
});
|
||||
|
||||
QUnit.test("formatInteger", function (assert) {
|
||||
assert.strictEqual(formatInteger(false), "");
|
||||
assert.strictEqual(formatInteger(0), "0");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
assert.strictEqual(formatInteger(1000000), "1,000,000");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
assert.strictEqual(formatInteger(106500), "1,06,500");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
assert.strictEqual(formatInteger(106500), "106,50,0");
|
||||
|
||||
const options = { grouping: [2, 0], thousandsSep: "€" };
|
||||
assert.strictEqual(formatInteger(6000, options), "60€00");
|
||||
});
|
||||
|
||||
QUnit.test("formatMany2one", function (assert) {
|
||||
assert.strictEqual(formatMany2one(false), "");
|
||||
assert.strictEqual(formatMany2one([false, "M2O value"]), "M2O value");
|
||||
assert.strictEqual(formatMany2one([1, "M2O value"]), "M2O value");
|
||||
assert.strictEqual(formatMany2one([1, "M2O value"], { escape: true }), "M2O%20value");
|
||||
});
|
||||
|
||||
QUnit.test("formatX2many", function (assert) {
|
||||
// Results are cast as strings since they're lazy translated.
|
||||
assert.strictEqual(String(formatX2many({ currentIds: [] })), "No records");
|
||||
assert.strictEqual(String(formatX2many({ currentIds: [1] })), "1 record");
|
||||
assert.strictEqual(String(formatX2many({ currentIds: [1, 3] })), "2 records");
|
||||
});
|
||||
|
||||
QUnit.test("formatMonetary", function (assert) {
|
||||
patchWithCleanup(session.currencies, {
|
||||
10: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
11: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
12: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "&",
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(formatMonetary(false), "");
|
||||
assert.strictEqual(formatMonetary(200), "200.00");
|
||||
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 10 }), "1,234,567.65\u00a0€");
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 11 }), "$\u00a01,234,567.65");
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 44 }), "1,234,567.65");
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 10, noSymbol: true }),
|
||||
"1,234,567.65"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(8.0, { currencyId: 10, humanReadable: true }),
|
||||
"8.00\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 10, humanReadable: true }),
|
||||
"1.23M\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1990000.001, { currencyId: 10, humanReadable: true }),
|
||||
"1.99M\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 44, digits: [69, 1] }),
|
||||
"1,234,567.7"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 11, digits: [69, 1] }),
|
||||
"$\u00a01,234,567.7",
|
||||
"options digits should take over currency digits when both are defined"
|
||||
);
|
||||
|
||||
// GES TODO do we keep below behavior ?
|
||||
// with field and data
|
||||
// const field = {
|
||||
// type: "monetary",
|
||||
// currency_field: "c_x",
|
||||
// };
|
||||
// let data = {
|
||||
// c_x: { res_id: 11 },
|
||||
// c_y: { res_id: 12 },
|
||||
// };
|
||||
// assert.strictEqual(formatMonetary(200, { field, currencyId: 10, data }), "200.00 €");
|
||||
// assert.strictEqual(formatMonetary(200, { field, data }), "$ 200.00");
|
||||
// assert.strictEqual(formatMonetary(200, { field, currencyField: "c_y", data }), "200.00 &");
|
||||
//
|
||||
// const floatField = { type: "float" };
|
||||
// data = {
|
||||
// currency_id: { res_id: 11 },
|
||||
// };
|
||||
// assert.strictEqual(formatMonetary(200, { field: floatField, data }), "$ 200.00");
|
||||
});
|
||||
|
||||
QUnit.test("formatMonetary without currency", function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
currencies: {},
|
||||
});
|
||||
assert.deepEqual(
|
||||
formatMonetary(1234567.654, { currencyId: 10, humanReadable: true }),
|
||||
"1.23M"
|
||||
);
|
||||
assert.deepEqual(formatMonetary(1234567.654, { currencyId: 10 }), "1,234,567.65");
|
||||
});
|
||||
|
||||
QUnit.test("formatPercentage", function (assert) {
|
||||
assert.strictEqual(formatPercentage(false), "0%");
|
||||
assert.strictEqual(formatPercentage(0), "0%");
|
||||
assert.strictEqual(formatPercentage(0.5), "50%");
|
||||
|
||||
assert.strictEqual(formatPercentage(1), "100%");
|
||||
|
||||
assert.strictEqual(formatPercentage(-0.2), "-20%");
|
||||
assert.strictEqual(formatPercentage(2.5), "250%");
|
||||
|
||||
assert.strictEqual(formatPercentage(0.125), "12.5%");
|
||||
assert.strictEqual(formatPercentage(0.666666), "66.67%");
|
||||
assert.strictEqual(formatPercentage(125), "12500%");
|
||||
|
||||
assert.strictEqual(formatPercentage(50, { humanReadable: true }), "5k%");
|
||||
assert.strictEqual(formatPercentage(0.5, { noSymbol: true }), "50");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 0], decimalPoint: ",", thousandsSep: "." });
|
||||
assert.strictEqual(formatPercentage(0.125), "12,5%");
|
||||
assert.strictEqual(formatPercentage(0.666666), "66,67%");
|
||||
});
|
||||
|
||||
QUnit.test("formatReference", function (assert) {
|
||||
assert.strictEqual(formatReference(false), "");
|
||||
const value = { resModel: "product", resId: 2, displayName: "Chair" };
|
||||
assert.strictEqual(formatReference(value), "Chair");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { onMounted } from "@odoo/owl";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { GaugeField } from "@web/views/fields/gauge/gauge_field";
|
||||
import { setupChartJsForTests } from "../graph/graph_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer({ string: "int_field" });
|
||||
another_int_field = fields.Integer({ string: "another_int_field" });
|
||||
_records = [
|
||||
{ id: 1, int_field: 10, another_int_field: 45 },
|
||||
{ id: 2, int_field: 4, another_int_field: 10 },
|
||||
];
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, User]);
|
||||
|
||||
setupChartJsForTests();
|
||||
|
||||
test("GaugeField in kanban view", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<field name="another_int_field"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="int_field" widget="gauge" options="{'max_field': 'another_int_field'}"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2);
|
||||
expect(".o_field_widget[name=int_field] .oe_gauge canvas").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_gauge_value")).toEqual(["10", "4"]);
|
||||
});
|
||||
|
||||
test("GaugeValue supports max_value option", async () => {
|
||||
patchWithCleanup(GaugeField.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onMounted(() => {
|
||||
expect.step("gauge mounted");
|
||||
expect(this.chart.config.options.plugins.tooltip.callbacks.label({})).toBe(
|
||||
"Max: 120"
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Partner._records = Partner._records.slice(0, 1);
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div>
|
||||
<field name="int_field" widget="gauge" options="{'max_value': 120}"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect.verifySteps(["gauge mounted"]);
|
||||
expect(".o_field_widget[name=int_field] .oe_gauge canvas").toHaveCount(1);
|
||||
expect(".o_gauge_value").toHaveText("10");
|
||||
});
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
MockServer,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
display_name = fields.Char({ string: "Displayed name", searchable: true });
|
||||
p = fields.One2many({ string: "one2many field", relation: "partner", searchable: true });
|
||||
sequence = fields.Integer({ string: "Sequence", searchable: true });
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
p: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
p: [],
|
||||
sequence: 4,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
sequence: 9,
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("HandleField in x2m", async () => {
|
||||
Partner._records[0].p = [2, 4];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="p">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("td span.o_row_handle").toHaveText("", {
|
||||
message: "handle should not have any content",
|
||||
});
|
||||
expect(queryFirst("td span.o_row_handle")).toBeVisible({
|
||||
message: "handle should be invisible",
|
||||
});
|
||||
|
||||
expect("span.o_row_handle").toHaveCount(2, { message: "should have 2 handles" });
|
||||
|
||||
expect(queryFirst("td")).toHaveClass("o_handle_cell", {
|
||||
message: "column widget should be displayed in css class",
|
||||
});
|
||||
await click("td:eq(1)");
|
||||
await animationFrame();
|
||||
|
||||
expect("td:eq(0) span.o_row_handle").toHaveCount(1, {
|
||||
message: "content of the cell should have been replaced",
|
||||
});
|
||||
});
|
||||
|
||||
test("HandleField with falsy values", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_row_handle:visible").toHaveCount(MockServer.env["partner"].length, {
|
||||
message: "there should be a visible handle for each record",
|
||||
});
|
||||
});
|
||||
|
||||
test("HandleField in a readonly one2many", async () => {
|
||||
Partner._records[0].p = [1, 2, 4];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="p" readonly="1">
|
||||
<list editable="top">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</list>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_row_handle.o_disabled").toHaveCount(3, {
|
||||
message: "there should be 3 handles but they should be disabled from readonly",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
sequence: { type: "integer", string: "Sequence", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
p: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
p: [],
|
||||
sequence: 4,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
sequence: 9,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("HandleField");
|
||||
|
||||
QUnit.test("HandleField in x2m", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [2, 4];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("td span.o_row_handle").textContent,
|
||||
"",
|
||||
"handle should not have any content"
|
||||
);
|
||||
|
||||
assert.isVisible(
|
||||
target.querySelector("td span.o_row_handle"),
|
||||
"handle should be invisible"
|
||||
);
|
||||
|
||||
assert.containsN(target, "span.o_row_handle", 2, "should have 2 handles");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector("td"),
|
||||
"o_handle_cell",
|
||||
"column widget should be displayed in css class"
|
||||
);
|
||||
|
||||
assert.notStrictEqual(
|
||||
getComputedStyle(target.querySelector("td span.o_row_handle")).display,
|
||||
"none",
|
||||
"handle should be visible in edit mode"
|
||||
);
|
||||
|
||||
await click(target.querySelectorAll("td")[1]);
|
||||
assert.containsOnce(
|
||||
target.querySelector("td"),
|
||||
"span.o_row_handle",
|
||||
"content of the cell should have been replaced"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("HandleField with falsy values", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
const visibleRowHandles = [...target.querySelectorAll(".o_row_handle")].filter(
|
||||
(el) => getComputedStyle(el).display !== "none"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
visibleRowHandles,
|
||||
serverData.models.partner.records.length,
|
||||
"there should be a visible handle for each record"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("HandleField in a readonly one2many", async function (assert) {
|
||||
serverData.models.partner.records[0].p = [1, 2, 4];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p" readonly="1">
|
||||
<tree editable="top">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="display_name" />
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_row_handle", 3, "there should be 3 handles, one for each row");
|
||||
assert.strictEqual(
|
||||
getComputedStyle(target.querySelector("td span.o_row_handle")).display,
|
||||
"none",
|
||||
"handle should be invisible"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, pointerDown, queryAll, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
const RED_TEXT = /* html */ `<div class="kek" style="color:red">some text</div>`;
|
||||
const GREEN_TEXT = /* html */ `<div class="kek" style="color:green">hello</div>`;
|
||||
const BLUE_TEXT = /* html */ `<div class="kek" style="color:blue">hello world</div>`;
|
||||
|
||||
class Partner extends models.Model {
|
||||
txt = fields.Html({ string: "txt", trim: true });
|
||||
_records = [{ id: 1, txt: RED_TEXT }];
|
||||
}
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
defineModels([Partner, User]);
|
||||
|
||||
test("html fields are correctly rendered in form view (readonly)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" readonly="1" /></form>`,
|
||||
});
|
||||
|
||||
expect("div.kek").toHaveCount(1);
|
||||
expect(".o_field_html .kek").toHaveStyle({ color: "rgb(255, 0, 0)" });
|
||||
expect(".o_field_html").toHaveText("some text");
|
||||
});
|
||||
|
||||
test("html field with required attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" required="1"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_html textarea").toHaveCount(1, { message: "should have a text area" });
|
||||
await click(".o_field_html textarea");
|
||||
await edit("");
|
||||
await animationFrame();
|
||||
expect(".o_field_html textarea").toHaveValue("");
|
||||
|
||||
await clickSave();
|
||||
expect(queryFirst(".o_notification_content")).toHaveText("Missing required fields");
|
||||
});
|
||||
|
||||
test("html fields are correctly rendered (edit)", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" /></form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_html textarea").toHaveCount(1, { message: "should have a text area" });
|
||||
expect(".o_field_html textarea").toHaveValue(RED_TEXT);
|
||||
await click(".o_field_html textarea");
|
||||
await edit(GREEN_TEXT);
|
||||
await animationFrame();
|
||||
expect(".o_field_html textarea").toHaveValue(GREEN_TEXT);
|
||||
expect(".o_field_html .kek").toHaveCount(0);
|
||||
|
||||
await edit(BLUE_TEXT);
|
||||
await animationFrame();
|
||||
expect(".o_field_html textarea").toHaveValue(BLUE_TEXT);
|
||||
});
|
||||
|
||||
test("html fields are correctly rendered in list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="top"><field name="txt"/></list>`,
|
||||
});
|
||||
expect(".o_data_row [name='txt']").toHaveText("some text");
|
||||
expect(".o_data_row [name='txt'] .kek").toHaveStyle({ color: "rgb(255, 0, 0)" });
|
||||
|
||||
await click(".o_data_row [name='txt']");
|
||||
await animationFrame();
|
||||
expect(".o_data_row [name='txt'] textarea").toHaveValue(
|
||||
'<div class="kek" style="color:red">some text</div>'
|
||||
);
|
||||
});
|
||||
|
||||
test("html field displays an empty string for the value false in list view", async () => {
|
||||
Partner._records[0].txt = false;
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<list editable="top"><field name="txt"/></list>`,
|
||||
});
|
||||
|
||||
expect(".o_data_row [name='txt']").toHaveText("");
|
||||
|
||||
await click(".o_data_row [name='txt']");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row [name='txt'] textarea").toHaveValue("");
|
||||
});
|
||||
|
||||
test("html fields are correctly rendered in kanban view", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban class="o_kanban_test">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="txt"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(".kek").toHaveText("some text");
|
||||
expect(".kek").toHaveStyle({ color: "rgb(255, 0, 0)" });
|
||||
});
|
||||
|
||||
test("field html translatable", async () => {
|
||||
expect.assertions(10);
|
||||
|
||||
Partner._fields.txt = fields.Html({ string: "txt", trim: true, translate: true });
|
||||
|
||||
serverState.lang = "en_US";
|
||||
serverState.multiLang = true;
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
onRpc("get_field_translations", function ({ args }) {
|
||||
expect(args).toEqual([[1], "txt"], {
|
||||
message: "should translate the txt field of the record",
|
||||
});
|
||||
return [
|
||||
[
|
||||
{ lang: "en_US", source: "first paragraph", value: "first paragraph" },
|
||||
{
|
||||
lang: "en_US",
|
||||
source: "second paragraph",
|
||||
value: "second paragraph",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "first paragraph",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "second paragraph",
|
||||
value: "deuxième paragraphe",
|
||||
},
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: true },
|
||||
];
|
||||
});
|
||||
onRpc("get_installed", () => [
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
]);
|
||||
onRpc("update_field_translations", ({ args }) => {
|
||||
expect(args).toEqual(
|
||||
[
|
||||
[1],
|
||||
"txt",
|
||||
{
|
||||
en_US: { "first paragraph": "first paragraph modified" },
|
||||
fr_BE: {
|
||||
"first paragraph": "premier paragraphe modifié",
|
||||
"second paragraph": "deuxième paragraphe modifié",
|
||||
},
|
||||
},
|
||||
],
|
||||
{ message: "the new translation value should be written" }
|
||||
);
|
||||
return [];
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" widget="html"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("[name=txt] textarea").toHaveClass("o_field_translate");
|
||||
await contains("[name=txt] textarea").click();
|
||||
expect(".o_field_html .btn.o_field_translate").toHaveCount(1, {
|
||||
message: "should have a translate button",
|
||||
});
|
||||
expect(".o_field_html .btn.o_field_translate").toHaveText("EN", {
|
||||
message: "the button should have as test the current language",
|
||||
});
|
||||
|
||||
await click(".o_field_html .btn.o_field_translate");
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1, { message: "a translate modal should be visible" });
|
||||
expect(".translation").toHaveCount(4, { message: "four rows should be visible" });
|
||||
|
||||
const translations = queryAll(".modal .o_translation_dialog .translation input");
|
||||
|
||||
const enField1 = translations[0];
|
||||
expect(enField1).toHaveValue("first paragraph", {
|
||||
message: "first part of english translation should be filled",
|
||||
});
|
||||
await click(enField1);
|
||||
await edit("first paragraph modified");
|
||||
|
||||
const frField1 = translations[2];
|
||||
expect(frField1).toHaveValue("", {
|
||||
message: "first part of french translation should not be filled",
|
||||
});
|
||||
await click(frField1);
|
||||
await edit("premier paragraphe modifié");
|
||||
|
||||
const frField2 = translations[3];
|
||||
expect(frField2).toHaveValue("deuxième paragraphe", {
|
||||
message: "second part of french translation should be filled",
|
||||
});
|
||||
|
||||
await click(frField2);
|
||||
await edit("deuxième paragraphe modifié");
|
||||
|
||||
await click(".modal button.btn-primary"); // save
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("html fields: spellcheck is disabled on blur", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `<form><field name="txt" /></form>`,
|
||||
});
|
||||
|
||||
const textarea = queryFirst(".o_field_html textarea");
|
||||
expect(textarea).toHaveProperty("spellcheck", true, {
|
||||
message: "by default, spellcheck is enabled",
|
||||
});
|
||||
await click(textarea);
|
||||
|
||||
await edit("nev walue");
|
||||
await pointerDown(document.body);
|
||||
await animationFrame();
|
||||
expect(textarea).toHaveProperty("spellcheck", false, {
|
||||
message: "spellcheck is disabled once the field has lost its focus",
|
||||
});
|
||||
|
||||
await pointerDown(textarea);
|
||||
|
||||
expect(textarea).toHaveProperty("spellcheck", true, {
|
||||
message: "spellcheck is re-enabled once the field is focused",
|
||||
});
|
||||
});
|
||||
|
||||
test("Setting an html field to empty string is saved as a false value", async () => {
|
||||
expect.assertions(1);
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].txt).toBe(false, { message: "the txt value should be false" });
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(".o_field_widget[name=txt] textarea");
|
||||
await edit("");
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("html field: correct value is used to evaluate the modifiers", async () => {
|
||||
Partner._fields.foo = fields.Char({
|
||||
string: "foo",
|
||||
onChange: (obj) => {
|
||||
if (obj.foo === "a") {
|
||||
obj.txt = false;
|
||||
} else if (obj.foo === "b") {
|
||||
obj.txt = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Partner._records[0].foo = false;
|
||||
Partner._records[0].txt = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="txt" invisible="'' == txt"/>
|
||||
</form>`,
|
||||
});
|
||||
expect("[name='txt'] textarea").toHaveCount(1);
|
||||
|
||||
await click("[name='foo'] input");
|
||||
await edit("a", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect("[name='txt'] textarea").toHaveCount(1);
|
||||
|
||||
await edit("b", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect("[name='txt'] textarea").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { HtmlField } from "@web/views/fields/html/html_field";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { session } from "@web/session";
|
||||
|
||||
const RED_TEXT = /* html */ `<div class="kek" style="color:red">some text</div>`;
|
||||
const GREEN_TEXT = /* html */ `<div class="kek" style="color:green">hello</div>`;
|
||||
const BLUE_TEXT = /* html */ `<div class="kek" style="color:blue">hello world</div>`;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Fields", ({ beforeEach }) => {
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
txt: { string: "txt", type: "html", trim: true },
|
||||
},
|
||||
records: [{ id: 1, txt: RED_TEXT }],
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
// Explicitly removed by web_editor, we need to add it back
|
||||
registry.category("fields").add("html", HtmlField, { force: true });
|
||||
});
|
||||
|
||||
QUnit.module("HtmlField");
|
||||
|
||||
QUnit.test("html fields are correctly rendered in form view (readonly)", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `<form><field name="txt" readonly="1" /></form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "div.kek");
|
||||
assert.strictEqual(target.querySelector(".o_field_html .kek").style.color, "red");
|
||||
assert.strictEqual(target.querySelector(".o_field_html").textContent, "some text");
|
||||
});
|
||||
|
||||
QUnit.test("html field with required attribute", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `<form><field name="txt" required="1"/></form>`,
|
||||
});
|
||||
|
||||
const textarea = target.querySelector(".o_field_html textarea");
|
||||
assert.ok(textarea, "should have a text area");
|
||||
|
||||
await editInput(textarea, null, "");
|
||||
assert.strictEqual(textarea.value, "");
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_title").textContent,
|
||||
"Invalid fields: "
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_content").innerHTML,
|
||||
"<ul><li>txt</li></ul>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("html fields are correctly rendered (edit)", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `<form><field name="txt" /></form>`,
|
||||
});
|
||||
|
||||
const textarea = target.querySelector(".o_field_html textarea");
|
||||
assert.ok(textarea, "should have a text area");
|
||||
assert.strictEqual(textarea.value, RED_TEXT);
|
||||
|
||||
await editInput(textarea, null, GREEN_TEXT);
|
||||
assert.strictEqual(textarea.value, GREEN_TEXT);
|
||||
assert.containsNone(target.querySelector(".o_field_html"), ".kek");
|
||||
|
||||
await editInput(textarea, null, BLUE_TEXT);
|
||||
assert.strictEqual(textarea.value, BLUE_TEXT);
|
||||
});
|
||||
|
||||
QUnit.test("html fields are correctly rendered in list view", async (assert) => {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="txt"/>
|
||||
</tree>`,
|
||||
});
|
||||
const txt = target.querySelector(".o_data_row [name='txt']");
|
||||
assert.strictEqual(txt.textContent, "some text");
|
||||
assert.strictEqual(txt.querySelector(".kek").style.color, "red");
|
||||
|
||||
await click(target.querySelector(".o_data_row [name='txt']"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row [name='txt'] textarea").value,
|
||||
'<div class="kek" style="color:red">some text</div>'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"html field displays an empty string for the value false in list view",
|
||||
async (assert) => {
|
||||
serverData.models.partner.records[0].txt = false;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="txt"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_data_row [name='txt']").textContent, "");
|
||||
|
||||
await click(target.querySelector(".o_data_row [name='txt']"));
|
||||
assert.strictEqual(target.querySelector(".o_data_row [name='txt'] textarea").value, "");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("html fields are correctly rendered in kanban view", async (assert) => {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban class="o_kanban_test">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="txt"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
const txt = target.querySelector(".kek");
|
||||
assert.strictEqual(txt.textContent, "some text");
|
||||
assert.strictEqual(txt.style.color, "red");
|
||||
});
|
||||
|
||||
QUnit.test("field html translatable", async (assert) => {
|
||||
assert.expect(10);
|
||||
|
||||
serverData.models.partner.fields.txt.translate = true;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
|
||||
force: true,
|
||||
});
|
||||
patchWithCleanup(session.user_context, {
|
||||
lang: "en_US",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="txt" widget="html"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, { args, method, model }) {
|
||||
if (route === "/web/dataset/call_kw/partner/get_field_translations") {
|
||||
assert.deepEqual(
|
||||
args,
|
||||
[[1], "txt"],
|
||||
"should translate the txt field of the record"
|
||||
);
|
||||
return Promise.resolve([
|
||||
[
|
||||
{ lang: "en_US", source: "first paragraph", value: "first paragraph" },
|
||||
{
|
||||
lang: "en_US",
|
||||
source: "second paragraph",
|
||||
value: "second paragraph",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "first paragraph",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
lang: "fr_BE",
|
||||
source: "second paragraph",
|
||||
value: "deuxième paragraphe",
|
||||
},
|
||||
],
|
||||
{ translation_type: "char", translation_show_source: true },
|
||||
]);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
|
||||
return Promise.resolve([
|
||||
["en_US", "English"],
|
||||
["fr_BE", "French (Belgium)"],
|
||||
]);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/partner/update_field_translations") {
|
||||
assert.deepEqual(
|
||||
args,
|
||||
[
|
||||
[1],
|
||||
"txt",
|
||||
{
|
||||
en_US: { "first paragraph": "first paragraph modified" },
|
||||
fr_BE: {
|
||||
"first paragraph": "premier paragraphe modifié",
|
||||
"deuxième paragraphe": "deuxième paragraphe modifié",
|
||||
},
|
||||
},
|
||||
],
|
||||
"the new translation value should be written"
|
||||
);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector("[name=txt] textarea"), "o_field_translate");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_html .btn.o_field_translate",
|
||||
"should have a translate button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_html .btn.o_field_translate").textContent,
|
||||
"EN",
|
||||
"the button should have as test the current language"
|
||||
);
|
||||
await click(target, ".o_field_html .btn.o_field_translate");
|
||||
|
||||
assert.containsOnce(target, ".modal", "a translate modal should be visible");
|
||||
assert.containsN(target, ".translation", 4, "four rows should be visible");
|
||||
|
||||
const translations = target.querySelectorAll(
|
||||
".modal .o_translation_dialog .translation input"
|
||||
);
|
||||
|
||||
const enField1 = translations[0];
|
||||
assert.strictEqual(
|
||||
enField1.value,
|
||||
"first paragraph",
|
||||
"first part of english translation should be filled"
|
||||
);
|
||||
await editInput(enField1, null, "first paragraph modified");
|
||||
|
||||
const frField1 = translations[2];
|
||||
assert.strictEqual(
|
||||
frField1.value,
|
||||
"",
|
||||
"first part of french translation should not be filled"
|
||||
);
|
||||
await editInput(frField1, null, "premier paragraphe modifié");
|
||||
|
||||
const frField2 = translations[3];
|
||||
assert.strictEqual(
|
||||
frField2.value,
|
||||
"deuxième paragraphe",
|
||||
"second part of french translation should be filled"
|
||||
);
|
||||
await editInput(frField2, null, "deuxième paragraphe modifié");
|
||||
|
||||
await click(target, ".modal button.btn-primary"); // save
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Report extends models.Model {
|
||||
int_field = fields.Integer();
|
||||
html_field = fields.Html();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
html_field: /* html */ `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { color : rgb(255, 0, 0); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nice_div"><p>Some content</p></div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Report]);
|
||||
|
||||
test("IframeWrapperField in form view with onchange", async () => {
|
||||
Report._onChanges.int_field = (record) => {
|
||||
record.html_field = record.html_field.replace("Some content", "New content");
|
||||
};
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "report",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="html_field" widget="iframe_wrapper"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("iframe:iframe .nice_div:first").toHaveInnerHTML("<p>Some content</p>");
|
||||
expect("iframe:iframe .nice_div p:first").toHaveStyle({
|
||||
color: "rgb(255, 0, 0)",
|
||||
});
|
||||
await click(".o_field_widget[name=int_field] input");
|
||||
await edit(264, { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect(queryFirst("iframe:iframe .nice_div")).toHaveInnerHTML("<p>New content</p>");
|
||||
});
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
report: {
|
||||
fields: {
|
||||
int_field: { string: "Int Field", type: "integer" },
|
||||
html_field: { string: "Content of report", type: "html" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
html_field: `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { color : rgb(255, 0, 0); }
|
||||
</style>
|
||||
<head>
|
||||
<body>
|
||||
<div class="nice_div"><p>Some content</p></div>
|
||||
</body>
|
||||
</html>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("IframeWrapperField");
|
||||
|
||||
QUnit.test("IframeWrapperField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "report",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="html_field" widget="iframe_wrapper"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const iframeDoc = target.querySelector("iframe").contentDocument;
|
||||
assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "<p>Some content</p>");
|
||||
assert.strictEqual($(iframeDoc).find(".nice_div p").css("color"), "rgb(255, 0, 0)");
|
||||
});
|
||||
|
||||
QUnit.test("IframeWrapperField in form view with onchange", async function (assert) {
|
||||
serverData.models.report.onchanges = {
|
||||
int_field(record) {
|
||||
record.html_field = record.html_field.replace("Some content", "New content");
|
||||
},
|
||||
};
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "report",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="html_field" widget="iframe_wrapper"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const iframeDoc = target.querySelector("iframe").contentDocument;
|
||||
assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "<p>Some content</p>");
|
||||
assert.strictEqual($(iframeDoc).find(".nice_div p").css("color"), "rgb(255, 0, 0)");
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", 264);
|
||||
assert.strictEqual(iframeDoc.querySelector(".nice_div").innerHTML, "<p>New content</p>");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,880 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
edit,
|
||||
manuallyDispatchProgrammaticEvent,
|
||||
queryAll,
|
||||
queryFirst,
|
||||
setInputFiles,
|
||||
waitFor,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers, mockDate } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
pagerNext,
|
||||
contains,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const MY_IMAGE =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
||||
const PRODUCT_IMAGE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
async function setFiles(files, name = "document") {
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setInputFiles(files);
|
||||
await waitFor(`div[name=${name}] img[data-src^="data:image/"]`, { timeout: 1000 });
|
||||
}
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
timmy = fields.Many2many({ relation: "partner.type" });
|
||||
foo = fields.Char();
|
||||
document = fields.Binary();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "first record", timmy: [], document: "coucou==" },
|
||||
{ id: 2, name: "second record", timmy: [] },
|
||||
{ id: 4, name: "aaa" },
|
||||
];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
_name = "partner.type";
|
||||
|
||||
name = fields.Char();
|
||||
color = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{ id: 12, name: "gold", color: 2 },
|
||||
{ id: 14, name: "silver", color: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
test("ImageField is correctly rendered", async () => {
|
||||
expect.assertions(10);
|
||||
|
||||
Partner._records[0].write_date = "2017-02-08 10:00:00";
|
||||
Partner._records[0].document = MY_IMAGE;
|
||||
|
||||
onRpc("web_read", ({ kwargs }) => {
|
||||
expect(kwargs.specification).toEqual(
|
||||
{
|
||||
display_name: {},
|
||||
document: {},
|
||||
write_date: {},
|
||||
},
|
||||
{
|
||||
message:
|
||||
"The fields document, name and write_date should be present when reading an image",
|
||||
}
|
||||
);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='document']").toHaveClass("o_field_image", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveCount(1, {
|
||||
message: "the widget should contain an image",
|
||||
});
|
||||
expect('div[name="document"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{ message: "the image should have the correct src" }
|
||||
);
|
||||
expect(".o_field_widget[name='document'] img").toHaveClass("img-fluid", {
|
||||
message: "the image should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveAttribute("width", "90", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveStyle(
|
||||
{
|
||||
maxWidth: "90px",
|
||||
width: "90px",
|
||||
height: "90px",
|
||||
},
|
||||
{
|
||||
message: "the image should correctly set its attributes",
|
||||
}
|
||||
);
|
||||
expect(".o_field_image .o_select_file_button").toHaveCount(1, {
|
||||
message: "the image can be edited",
|
||||
});
|
||||
expect(".o_field_image .o_clear_file_button").toHaveCount(1, {
|
||||
message: "the image can be deleted",
|
||||
});
|
||||
expect("input.o_input_file").toHaveAttribute("accept", "image/*", {
|
||||
message:
|
||||
'the default value for the attribute "accept" on the "image" widget must be "image/*"',
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField with img_class option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'img_class': 'my_custom_class'}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_image img").toHaveClass("my_custom_class");
|
||||
});
|
||||
|
||||
test("ImageField with alt attribute", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" alt="something"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='document'] img").toHaveAttribute("alt", "something", {
|
||||
message: "the image should correctly set its alt attribute",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField on a many2one", async () => {
|
||||
Partner._fields.parent_id = fields.Many2one({ relation: "partner" });
|
||||
Partner._records[1].parent_id = 1;
|
||||
|
||||
mockDate("2017-02-06 10:00:00");
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="parent_id" widget="image" options="{'preview_image': 'document'}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=parent_id] img").toHaveCount(1);
|
||||
expect('div[name="parent_id"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
`${getOrigin()}/web/image/partner/1/document?unique=1486375200000`
|
||||
);
|
||||
expect(".o_field_widget[name='parent_id'] img").toHaveAttribute("alt", "first record");
|
||||
});
|
||||
|
||||
test("url should not use the record last updated date when the field is related", async () => {
|
||||
Partner._fields.related = fields.Binary({ related: "parent_id.document" });
|
||||
Partner._fields.parent_id = fields.Many2one({ relation: "partner" });
|
||||
Partner._records[1].parent_id = 1;
|
||||
Partner._records[0].write_date = "2017-02-04 10:00:00";
|
||||
Partner._records[0].document = "3 kb";
|
||||
|
||||
mockDate("2017-02-06 10:00:00");
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="related" widget="image" readonly="0"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const initialUnique = Number(getUnique(queryFirst('div[name="related"] img')));
|
||||
expect(DateTime.fromMillis(initialUnique).hasSame(DateTime.fromISO("2017-02-06"), "days")).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
await click(".o_field_widget[name='foo'] input");
|
||||
await edit("grrr");
|
||||
await animationFrame();
|
||||
|
||||
expect(Number(getUnique(queryFirst('div[name="related"] img')))).toBe(initialUnique);
|
||||
|
||||
mockDate("2017-02-09 10:00:00");
|
||||
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setFiles(
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
),
|
||||
"related"
|
||||
);
|
||||
|
||||
expect("div[name=related] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
|
||||
const unique = Number(getUnique(queryFirst('div[name="related"] img')));
|
||||
expect(DateTime.fromMillis(unique).hasSame(DateTime.fromISO("2017-02-09"), "days")).toBe(true);
|
||||
});
|
||||
|
||||
test("url should use the record last updated date when the field is related on the same model", async () => {
|
||||
Partner._fields.related = fields.Binary({ related: "document" });
|
||||
Partner._records[0].write_date = "2017-02-04 10:00:00"; // 1486202400000
|
||||
Partner._records[0].document = "3 kb";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="related" widget="image"/>
|
||||
</form>`,
|
||||
});
|
||||
expect('div[name="related"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
`${getOrigin()}/web/image/partner/1/related?unique=1486202400000`
|
||||
);
|
||||
});
|
||||
|
||||
test("ImageField is correctly replaced when given an incorrect value", async () => {
|
||||
Partner._records[0].document = "incorrect_base64_value";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`div[name="document"] img`).toHaveAttribute(
|
||||
"data-src",
|
||||
"data:image/png;base64,incorrect_base64_value",
|
||||
{
|
||||
message: "the image has the invalid src by default",
|
||||
}
|
||||
);
|
||||
|
||||
// As GET requests can't occur in tests, we must generate an error
|
||||
// on the img element to check whether the data-src is replaced with
|
||||
// a placeholder, here knowing that the GET request would fail
|
||||
manuallyDispatchProgrammaticEvent(queryFirst('div[name="document"] img'), "error");
|
||||
await animationFrame();
|
||||
|
||||
expect('.o_field_widget[name="document"]').toHaveClass("o_field_image", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveCount(1, {
|
||||
message: "the widget should contain an image",
|
||||
});
|
||||
expect('div[name="document"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "the image should have the correct src" }
|
||||
);
|
||||
expect(".o_field_widget[name='document'] img").toHaveClass("img-fluid", {
|
||||
message: "the image should have the correct class",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveAttribute("width", "90", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
expect(".o_field_widget[name='document'] img").toHaveStyle("maxWidth: 90px", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
|
||||
expect(".o_field_image .o_select_file_button").toHaveCount(1, {
|
||||
message: "the image can be edited",
|
||||
});
|
||||
expect(".o_field_image .o_clear_file_button").toHaveCount(0, {
|
||||
message: "the image cannot be deleted as it has not been uploaded",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField preview is updated when an image is uploaded", async () => {
|
||||
const imageFile = new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect('div[name="document"] img').toHaveAttribute(
|
||||
"data-src",
|
||||
"data:image/png;base64,coucou==",
|
||||
{ message: "the image should have the initial src" }
|
||||
);
|
||||
// Whitebox: replace the event target before the event is handled by the field so that we can modify
|
||||
// the files that it will take into account. This relies on the fact that it reads the files from
|
||||
// event.target and not from a direct reference to the input element.
|
||||
await click(".o_select_file_button");
|
||||
await setInputFiles(imageFile);
|
||||
// It can take some time to encode the data as a base64 url
|
||||
await runAllTimers();
|
||||
// Wait for a render
|
||||
await animationFrame();
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{ message: "the image should have the new src" }
|
||||
);
|
||||
});
|
||||
|
||||
test("clicking save manually after uploading new image should change the unique of the image src", async () => {
|
||||
Partner._onChanges.foo = () => {};
|
||||
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000, 1659695820000
|
||||
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
|
||||
let index = 0;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
args[1].write_date = lastUpdates[index];
|
||||
args[1].document = "4 kb";
|
||||
index++;
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setFiles(
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await click(".o_field_widget[name='foo'] input");
|
||||
await edit("grrr");
|
||||
await animationFrame();
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
|
||||
// Change the image again. After clicking save, it should have the correct new url.
|
||||
await click("input[type=file]", { visible: false });
|
||||
await setFiles(
|
||||
new File(
|
||||
[Uint8Array.from([...atob(PRODUCT_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file2.gif",
|
||||
{ type: "gif" }
|
||||
)
|
||||
);
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/gif;base64,${PRODUCT_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659695820000");
|
||||
});
|
||||
|
||||
test("save record with image field modified by onchange", async () => {
|
||||
Partner._onChanges.foo = (data) => {
|
||||
data.document = MY_IMAGE;
|
||||
};
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000
|
||||
const lastUpdates = ["2022-08-05 09:37:00"];
|
||||
let index = 0;
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
args[1].write_date = lastUpdates[index];
|
||||
args[1].document = "3 kb";
|
||||
index++;
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
await click("[name='foo'] input");
|
||||
await edit("grrr", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect("div[name=document] img").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
});
|
||||
|
||||
test("ImageField: option accepted_file_extensions", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'accepted_file_extensions': '.png,.jpeg'}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
// The view must be in edit mode
|
||||
expect("input.o_input_file").toHaveAttribute("accept", ".png,.jpeg", {
|
||||
message: "the input should have the correct ``accept`` attribute",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField: set 0 width/height in the size option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [0, 0]}" />
|
||||
<field name="document" widget="image" options="{'size': [0, 50]}" />
|
||||
<field name="document" widget="image" options="{'size': [50, 0]}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const imgs = queryAll(".o_field_widget img");
|
||||
|
||||
expect([imgs[0].attributes.width, imgs[0].attributes.height]).toEqual([undefined, undefined], {
|
||||
message: "if both size are set to 0, both attributes are undefined",
|
||||
});
|
||||
|
||||
expect([imgs[1].attributes.width, imgs[1].attributes.height.value]).toEqual([undefined, "50"], {
|
||||
message: "if only the width is set to 0, the width attribute is not set on the img",
|
||||
});
|
||||
expect([
|
||||
imgs[1].style.width,
|
||||
imgs[1].style.maxWidth,
|
||||
imgs[1].style.height,
|
||||
imgs[1].style.maxHeight,
|
||||
]).toEqual(["auto", "100%", "", "50px"], {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
|
||||
expect([imgs[2].attributes.width.value, imgs[2].attributes.height]).toEqual(["50", undefined], {
|
||||
message: "if only the height is set to 0, the height attribute is not set on the img",
|
||||
});
|
||||
expect([
|
||||
imgs[2].style.width,
|
||||
imgs[2].style.maxWidth,
|
||||
imgs[2].style.height,
|
||||
imgs[2].style.maxHeight,
|
||||
]).toEqual(["", "50px", "auto", "100%"], {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField: zoom and zoom_delay options (readonly)", async () => {
|
||||
Partner._records[0].document = MY_IMAGE;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" readonly="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
// data-tooltip attribute is used by the tooltip service
|
||||
expect(".o_field_image img").toHaveAttribute(
|
||||
"data-tooltip-info",
|
||||
`{"url":"data:image/png;base64,${MY_IMAGE}"}`,
|
||||
{ message: "shows a tooltip on hover" }
|
||||
);
|
||||
expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", {
|
||||
message: "tooltip has the right delay",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField: zoom and zoom_delay options (edit)", async () => {
|
||||
Partner._records[0].document = "3 kb";
|
||||
Partner._records[0].write_date = "2022-08-05 08:37:00";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_field_image img").toHaveAttribute(
|
||||
"data-tooltip-info",
|
||||
`{"url":"${getOrigin()}/web/image/partner/1/document?unique=1659688620000"}`,
|
||||
{ message: "tooltip show the full image from the field value" }
|
||||
);
|
||||
expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", {
|
||||
message: "tooltip has the right delay",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField displays the right images with zoom and preview_image options (readonly)", async () => {
|
||||
Partner._records[0].document = "3 kb";
|
||||
Partner._records[0].write_date = "2022-08-05 08:37:00";
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'preview_image': 'document_preview', 'zoom_delay': 600}" readonly="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_field_image img").toHaveAttribute(
|
||||
"data-tooltip-info",
|
||||
`{"url":"${getOrigin()}/web/image/partner/1/document?unique=1659688620000"}`,
|
||||
{ message: "tooltip show the full image from the field value" }
|
||||
);
|
||||
expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", {
|
||||
message: "tooltip has the right delay",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField in subviews is loaded correctly", async () => {
|
||||
Partner._records[0].write_date = "2017-02-08 10:00:00";
|
||||
Partner._records[0].document = MY_IMAGE;
|
||||
PartnerType._fields.image = fields.Binary({});
|
||||
PartnerType._records[0].image = PRODUCT_IMAGE;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}" />
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name" />
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image" />
|
||||
</form>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(`img[data-src="data:image/png;base64,${MY_IMAGE}"]`).toHaveCount(1);
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost):not(.o-kanban-button-new)").toHaveCount(1);
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(".o_kanban_record:not(.o_kanban_ghost):not(.o-kanban-button-new)");
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(1, { message: "The modal should have opened" });
|
||||
|
||||
expect(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("ImageField in x2many list is loaded correctly", async () => {
|
||||
PartnerType._fields.image = fields.Binary({});
|
||||
PartnerType._records[0].image = PRODUCT_IMAGE;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<list>
|
||||
<field name="image" widget="image" />
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect("tr.o_data_row").toHaveCount(1, {
|
||||
message: "There should be one record in the many2many",
|
||||
});
|
||||
expect(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`).toHaveCount(1, {
|
||||
message: "The list's image is in the DOM",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField with required attribute", async () => {
|
||||
onRpc("create", () => {
|
||||
throw new Error("Should not do a create RPC with unset required image field");
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_form_view .o_form_editable").toHaveCount(1, {
|
||||
message: "form view should still be editable",
|
||||
});
|
||||
expect(".o_field_widget").toHaveClass("o_field_invalid", {
|
||||
message: "image field should be displayed as invalid",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageField is reset when changing record", async () => {
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const imageFile = new File([imageData], "fake_file.png", { type: "png" });
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "image field should not be set" }
|
||||
);
|
||||
|
||||
await setFiles(imageFile);
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{
|
||||
message: "image field should be set",
|
||||
}
|
||||
);
|
||||
|
||||
await clickSave();
|
||||
await click(".o_control_panel_main_buttons .o_form_button_create");
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "image field should be reset" }
|
||||
);
|
||||
|
||||
await setFiles(imageFile);
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
{
|
||||
message: "image field should be set",
|
||||
}
|
||||
);
|
||||
});
|
||||
test("unique in url doesn't change on onchange", async () => {
|
||||
Partner._onChanges.foo = () => {};
|
||||
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00";
|
||||
|
||||
onRpc(({ method, args }) => {
|
||||
expect.step(method);
|
||||
if (method === "web_save") {
|
||||
args[1].write_date = "2022-08-05 09:37:00"; // 1659692220000
|
||||
}
|
||||
});
|
||||
await mountView({
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect.verifySteps(["get_views", "web_read"]);
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
expect.verifySteps([]);
|
||||
// same unique as before
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
await click(".o_field_widget[name='foo'] input");
|
||||
await edit("grrr", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
expect.verifySteps(["onchange"]);
|
||||
// also same unique
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
|
||||
await clickSave();
|
||||
expect.verifySteps(["web_save"]);
|
||||
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
});
|
||||
|
||||
test("unique in url change on record change", async () => {
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00";
|
||||
|
||||
const rec2 = Partner._records.find((rec) => rec.id === 2);
|
||||
rec2.document = "3 kb";
|
||||
rec2.write_date = "2022-08-05 09:37:00";
|
||||
|
||||
await mountView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
await pagerNext();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000");
|
||||
});
|
||||
|
||||
test("unique in url does not change on record change if reload option is set to false", async () => {
|
||||
const rec = Partner._records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.write_date = "2022-08-05 08:37:00";
|
||||
|
||||
await mountView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" options="{'reload': false}" />
|
||||
<field name="write_date" readonly="0"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
await contains("div[name='write_date'] > div > button").click();
|
||||
await edit("2022-08-05 08:39:00", { confirm: "enter" });
|
||||
await animationFrame();
|
||||
await clickSave();
|
||||
expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000");
|
||||
});
|
||||
|
||||
test("convert image to webp", async () => {
|
||||
onRpc("ir.attachment", "create_unique", ({ args }) => {
|
||||
// This RPC call is done two times - once for storing webp and once for storing jpeg
|
||||
// This handles first RPC call to store webp
|
||||
if (!args[0][0].res_id) {
|
||||
// Here we check the image data we pass and generated data.
|
||||
// Also we check the file type
|
||||
expect(args[0][0].datas).not.toBe(imageData);
|
||||
expect(args[0][0].mimetype).toBe("image/webp");
|
||||
return [1];
|
||||
}
|
||||
// This handles second RPC call to store jpeg
|
||||
expect(args[0][0].datas).not.toBe(imageData);
|
||||
expect(args[0][0].mimetype).toBe("image/jpeg");
|
||||
return true;
|
||||
});
|
||||
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" options="{'convert_to_webp': True}" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const imageFile = new File([imageData], "fake_file.jpeg", { type: "jpeg" });
|
||||
expect("img[alt='Binary file']").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/static/img/placeholder.png",
|
||||
{ message: "image field should not be set" }
|
||||
);
|
||||
await setFiles(imageFile);
|
||||
});
|
||||
|
|
@ -1,963 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
nextTick,
|
||||
triggerEvent,
|
||||
clickSave,
|
||||
editInput,
|
||||
patchDate,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { pagerNext } from "@web/../tests/search/helpers";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const MY_IMAGE =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
||||
const PRODUCT_IMAGE =
|
||||
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: {
|
||||
string: "Displayed name",
|
||||
type: "char",
|
||||
searchable: true,
|
||||
},
|
||||
timmy: {
|
||||
string: "pokemon",
|
||||
type: "many2many",
|
||||
relation: "partner_type",
|
||||
searchable: true,
|
||||
},
|
||||
foo: { type: "char" },
|
||||
document: { string: "Binary", type: "binary" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
timmy: [],
|
||||
document: "coucou==",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
timmy: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
display_name: "aaa",
|
||||
},
|
||||
],
|
||||
},
|
||||
partner_type: {
|
||||
fields: {
|
||||
name: { string: "Partner Type", type: "char", searchable: true },
|
||||
color: { string: "Color index", type: "integer", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("ImageField");
|
||||
|
||||
QUnit.test("ImageField is correctly rendered", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
|
||||
serverData.models.partner.records[0].document = MY_IMAGE;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}" />
|
||||
</form>`,
|
||||
mockRPC(route, { args }) {
|
||||
if (route === "/web/dataset/call_kw/partner/read") {
|
||||
assert.deepEqual(
|
||||
args[1],
|
||||
["__last_update", "document", "display_name"],
|
||||
"The fields document, display_name and __last_update should be present when reading an image"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='document']"),
|
||||
"o_field_image",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name='document'] img",
|
||||
"the widget should contain an image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
"the image should have the correct src"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"img-fluid",
|
||||
"the image should have the correct class"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"width",
|
||||
"90",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='document'] img").style.maxWidth,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
|
||||
const computedStyle = window.getComputedStyle(
|
||||
target.querySelector(".o_field_widget[name='document'] img")
|
||||
);
|
||||
assert.strictEqual(
|
||||
computedStyle.width,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
computedStyle.height,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_image .o_select_file_button",
|
||||
"the image can be edited"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_image .o_clear_file_button",
|
||||
"the image can be deleted"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
"image/*",
|
||||
'the default value for the attribute "accept" on the "image" widget must be "image/*"'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"ImageField is correctly replaced when given an incorrect value",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].document = "incorrect_base64_value";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
"data:image/png;base64,incorrect_base64_value",
|
||||
"the image has the invalid src by default"
|
||||
);
|
||||
|
||||
// As GET requests can't occur in tests, we must generate an error
|
||||
// on the img element to check whether the data-src is replaced with
|
||||
// a placeholder, here knowing that the GET request would fail
|
||||
await triggerEvent(target, 'div[name="document"] img', "error");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector('.o_field_widget[name="document"]'),
|
||||
"o_field_image",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget[name='document'] img",
|
||||
"the widget should contain an image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
"/web/static/img/placeholder.png",
|
||||
"the image should have the correct src"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"img-fluid",
|
||||
"the image should have the correct class"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget[name='document'] img"),
|
||||
"width",
|
||||
"90",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='document'] img").style.maxWidth,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_image .o_select_file_button",
|
||||
"the image can be edited"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_image .o_clear_file_button",
|
||||
"the image cannot be deleted as it has not been uploaded"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("ImageField preview is updated when an image is uploaded", async function (assert) {
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="document"] img').dataset.src,
|
||||
"data:image/png;base64,coucou==",
|
||||
"the image should have the initial src"
|
||||
);
|
||||
// Whitebox: replace the event target before the event is handled by the field so that we can modify
|
||||
// the files that it will take into account. This relies on the fact that it reads the files from
|
||||
// event.target and not from a direct reference to the input element.
|
||||
const fileInput = target.querySelector("input[type=file]");
|
||||
const fakeInput = {
|
||||
files: [new File([imageData], "fake_file.png", { type: "png" })],
|
||||
};
|
||||
fileInput.addEventListener(
|
||||
"change",
|
||||
(ev) => {
|
||||
Object.defineProperty(ev, "target", { value: fakeInput });
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
// It can take some time to encode the data as a base64 url
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
// Wait for a render
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
"the image should have the new src"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"clicking save manually after uploading new image should change the unique of the image src",
|
||||
async function (assert) {
|
||||
serverData.models.partner.onchanges = { foo: () => {} };
|
||||
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000, 1659695820000
|
||||
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
|
||||
let index = 0;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>`,
|
||||
mockRPC(_route, { method, args }) {
|
||||
if (method === "write") {
|
||||
args[1].__last_update = lastUpdates[index];
|
||||
args[1].document = "4 kb";
|
||||
index++;
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659688620000"
|
||||
);
|
||||
|
||||
await editInput(
|
||||
target,
|
||||
"input[type=file]",
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659692220000"
|
||||
);
|
||||
|
||||
// Change the image again. After clicking save, it should have the correct new url.
|
||||
await editInput(
|
||||
target,
|
||||
"input[type=file]",
|
||||
new File(
|
||||
[Uint8Array.from([...atob(PRODUCT_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file2.gif",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/gif;base64,${PRODUCT_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659695820000"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("save record with image field modified by onchange", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: (data) => {
|
||||
data.document = MY_IMAGE;
|
||||
},
|
||||
};
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00"; // 1659688620000
|
||||
|
||||
// 1659692220000
|
||||
const lastUpdates = ["2022-08-05 09:37:00"];
|
||||
let index = 0;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="document" widget="image" />
|
||||
</form>`,
|
||||
mockRPC(_route, { method, args }) {
|
||||
if (method === "write") {
|
||||
args[1].__last_update = lastUpdates[index];
|
||||
args[1].document = "3 kb";
|
||||
index++;
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
await editInput(target, "[name='foo'] input", "grrr");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div[name=document] img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: option accepted_file_extensions", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'accepted_file_extensions': '.png,.jpeg'}" />
|
||||
</form>`,
|
||||
});
|
||||
// The view must be in edit mode
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
".png,.jpeg",
|
||||
"the input should have the correct ``accept`` attribute"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: set 0 width/height in the size option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [0, 0]}" />
|
||||
<field name="document" widget="image" options="{'size': [0, 50]}" />
|
||||
<field name="document" widget="image" options="{'size': [50, 0]}" />
|
||||
</form>`,
|
||||
});
|
||||
const imgs = target.querySelectorAll(".o_field_widget img");
|
||||
|
||||
assert.deepEqual(
|
||||
[imgs[0].attributes.width, imgs[0].attributes.height],
|
||||
[undefined, undefined],
|
||||
"if both size are set to 0, both attributes are undefined"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[imgs[1].attributes.width, imgs[1].attributes.height.value],
|
||||
[undefined, "50"],
|
||||
"if only the width is set to 0, the width attribute is not set on the img"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[
|
||||
imgs[1].style.width,
|
||||
imgs[1].style.maxWidth,
|
||||
imgs[1].style.height,
|
||||
imgs[1].style.maxHeight,
|
||||
],
|
||||
["auto", "100%", "", "50px"],
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[imgs[2].attributes.width.value, imgs[2].attributes.height],
|
||||
["50", undefined],
|
||||
"if only the height is set to 0, the height attribute is not set on the img"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[
|
||||
imgs[2].style.width,
|
||||
imgs[2].style.maxWidth,
|
||||
imgs[2].style.height,
|
||||
imgs[2].style.maxHeight,
|
||||
],
|
||||
["", "50px", "auto", "100%"],
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: zoom and zoom_delay options (readonly)", async (assert) => {
|
||||
serverData.models.partner.records[0].document = MY_IMAGE;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
// data-tooltip attribute is used by the tooltip service
|
||||
assert.strictEqual(
|
||||
JSON.parse(target.querySelector(".o_field_image img").dataset["tooltipInfo"]).url,
|
||||
`data:image/png;base64,${MY_IMAGE}`,
|
||||
"shows a tooltip on hover"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
|
||||
"600",
|
||||
"tooltip has the right delay"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField: zoom and zoom_delay options (edit)", async function (assert) {
|
||||
serverData.models.partner.records[0].document = "3 kb";
|
||||
serverData.models.partner.records[0].__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
JSON.parse(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipInfo"]
|
||||
).url.endsWith("/web/image?model=partner&id=1&field=document&unique=1659688620000"),
|
||||
"tooltip show the full image from the field value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
|
||||
"600",
|
||||
"tooltip has the right delay"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"ImageField displays the right images with zoom and preview_image options (readonly)",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].document = "3 kb";
|
||||
serverData.models.partner.records[0].__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'zoom': true, 'preview_image': 'document_preview', 'zoom_delay': 600}" readonly="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
JSON.parse(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipInfo"]
|
||||
).url.endsWith("/web/image?model=partner&id=1&field=document&unique=1659688620000"),
|
||||
"tooltip show the full image from the field value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
|
||||
"600",
|
||||
"tooltip has the right delay"
|
||||
);
|
||||
assert.ok(
|
||||
target
|
||||
.querySelector(".o_field_image img")
|
||||
.dataset.src.endsWith(
|
||||
"/web/image?model=partner&id=1&field=document_preview&unique=1659688620000"
|
||||
),
|
||||
"image src is the preview image given in option"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("ImageField in subviews is loaded correctly", async function (assert) {
|
||||
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
|
||||
serverData.models.partner.records[0].document = MY_IMAGE;
|
||||
serverData.models.partner_type.fields.image = {
|
||||
name: "image",
|
||||
type: "binary",
|
||||
};
|
||||
serverData.models.partner_type.records[0].image = PRODUCT_IMAGE;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}" />
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<field name="display_name" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<span>
|
||||
<t t-esc="record.display_name.value" />
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image" />
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, `img[data-src="data:image/png;base64,${MY_IMAGE}"]`);
|
||||
assert.containsOnce(target, ".o_kanban_record .oe_kanban_global_click");
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(target, ".oe_kanban_global_click");
|
||||
assert.containsOnce(target, ".modal", "The modal should have opened");
|
||||
|
||||
assert.containsOnce(target, `img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField in x2many list is loaded correctly", async function (assert) {
|
||||
serverData.models.partner_type.fields.image = {
|
||||
name: "image",
|
||||
type: "binary",
|
||||
};
|
||||
serverData.models.partner_type.records[0].image = PRODUCT_IMAGE;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<tree>
|
||||
<field name="image" widget="image" />
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "tr.o_data_row", "There should be one record in the many2many");
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`),
|
||||
"The list's image is in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField with required attribute", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>`,
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "create") {
|
||||
throw new Error("Should not do a create RPC with unset required image field");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.containsOnce(
|
||||
target.querySelector(".o_form_view"),
|
||||
".o_form_editable",
|
||||
"form view should still be editable"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_invalid",
|
||||
"image field should be displayed as invalid"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageField is reset when changing record", async function (assert) {
|
||||
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form>
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
async function setFiles() {
|
||||
const list = new DataTransfer();
|
||||
list.items.add(new File([imageData], "fake_file.png", { type: "png" }));
|
||||
const fileInput = target.querySelector("input[type=file]");
|
||||
fileInput.files = list.files;
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
// It can take some time to encode the data as a base64 url
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
// Wait for a render
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("img[data-alt='Binary file']").dataset.src,
|
||||
"/web/static/img/placeholder.png",
|
||||
"image field should not be set"
|
||||
);
|
||||
|
||||
await setFiles();
|
||||
assert.ok(
|
||||
target
|
||||
.querySelector("img[data-alt='Binary file']")
|
||||
.dataset.src.includes("data:image/png;base64"),
|
||||
"image field should be set"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
await click(target, ".o_form_button_create");
|
||||
assert.strictEqual(
|
||||
target.querySelector("img[data-alt='Binary file']").dataset.src,
|
||||
"/web/static/img/placeholder.png",
|
||||
"image field should be reset"
|
||||
);
|
||||
|
||||
await setFiles();
|
||||
assert.ok(
|
||||
target
|
||||
.querySelector("img[data-alt='Binary file']")
|
||||
.dataset.src.includes("data:image/png;base64"),
|
||||
"image field should be set"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("unique in url doesn't change on onchange", async (assert) => {
|
||||
serverData.models.partner.onchanges = {
|
||||
foo: () => {},
|
||||
};
|
||||
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" />
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>`,
|
||||
mockRPC(route, { method, args }) {
|
||||
assert.step(method);
|
||||
if (method === "write") {
|
||||
// 1659692220000
|
||||
args[1].__last_update = "2022-08-05 09:37:00";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["get_views", "read"]);
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
|
||||
assert.verifySteps([]);
|
||||
// same unique as before
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
|
||||
assert.verifySteps(["onchange"]);
|
||||
// also same unique
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
|
||||
await clickSave(target);
|
||||
assert.verifySteps(["write", "read"]);
|
||||
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
|
||||
});
|
||||
|
||||
QUnit.test("unique in url change on record change", async (assert) => {
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
const rec2 = serverData.models.partner.records.find((rec) => rec.id === 2);
|
||||
rec2.document = "3 kb";
|
||||
rec2.__last_update = "2022-08-05 09:37:00";
|
||||
|
||||
await makeView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
|
||||
await pagerNext(target);
|
||||
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"unique in url does not change on record change if no_reload option is set",
|
||||
async (assert) => {
|
||||
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
|
||||
rec.document = "3 kb";
|
||||
rec.__last_update = "2022-08-05 08:37:00";
|
||||
|
||||
await makeView({
|
||||
resIds: [1, 2],
|
||||
resId: 1,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="document" widget="image" required="1" options="{'no_reload': true}" />
|
||||
<field name="__last_update" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
function getUnique(target) {
|
||||
const src = target.dataset.src;
|
||||
return new URL(src).searchParams.get("unique");
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659688620000"
|
||||
);
|
||||
await editInput(
|
||||
target.querySelector(
|
||||
"div[name='__last_update'] > div > input",
|
||||
"2022-08-05 08:39:00"
|
||||
)
|
||||
);
|
||||
await click(target, ".o_form_button_save");
|
||||
assert.strictEqual(
|
||||
getUnique(target.querySelector(".o_field_image img")),
|
||||
"1659688620000"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"url should not use the record last updated date when the field is related",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.related = {
|
||||
name: "Binary",
|
||||
type: "binary",
|
||||
related: "user.image",
|
||||
};
|
||||
|
||||
serverData.models.partner.fields.user = {
|
||||
name: "User",
|
||||
type: "many2one",
|
||||
relation: "user",
|
||||
default: 1,
|
||||
};
|
||||
|
||||
serverData.models.user = {
|
||||
fields: {
|
||||
image: {
|
||||
name: "Image",
|
||||
type: "binary",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
image: "3 kb",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
|
||||
|
||||
patchDate(2017, 1, 6, 11, 0, 0);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" />
|
||||
<field name="user"/>
|
||||
<field name="related" widget="image"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
async mockRPC(route, { args }, performRpc) {
|
||||
if (route === "/web/dataset/call_kw/partner/read") {
|
||||
const res = await performRpc(...arguments);
|
||||
// The mockRPC doesn't implement related fields
|
||||
res[0].related = "3 kb";
|
||||
return res;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const initialUnique = Number(getUnique(target.querySelector(".o_field_image img")));
|
||||
assert.ok(
|
||||
DateTime.fromMillis(initialUnique).hasSame(DateTime.fromISO("2017-02-06"), "days")
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
|
||||
|
||||
// the unique should be the same
|
||||
assert.strictEqual(
|
||||
initialUnique,
|
||||
Number(getUnique(target.querySelector(".o_field_image img")))
|
||||
);
|
||||
|
||||
patchDate(2017, 1, 9, 11, 0, 0);
|
||||
await editInput(
|
||||
target,
|
||||
"input[type=file]",
|
||||
new File(
|
||||
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
|
||||
"fake_file.png",
|
||||
{ type: "png" }
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_image img").dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`
|
||||
);
|
||||
|
||||
patchDate(2017, 1, 9, 12, 0, 0);
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
const unique = Number(getUnique(target.querySelector(".o_field_image img")));
|
||||
assert.ok(DateTime.fromMillis(unique).hasSame(DateTime.fromISO("2017-02-09"), "days"));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
|
||||
const FR_FLAG_URL = "/base/static/img/country_flags/fr.png";
|
||||
const EN_FLAG_URL = "/base/static/img/country_flags/gb.png";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
foo = fields.Char();
|
||||
p = fields.One2many({ relation: "partner" });
|
||||
timmy = fields.Many2many({ relation: "partner.type" });
|
||||
|
||||
_records = [{ id: 1, foo: FR_FLAG_URL, timmy: [] }];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
name = fields.Char();
|
||||
color = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType, User]);
|
||||
|
||||
test("image fields are correctly rendered", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`div[name="foo"]`).toHaveClass("o_field_image_url", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveCount(1, {
|
||||
message: "the widget should contain an image",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("data-src", FR_FLAG_URL, {
|
||||
message: "the image should have the correct src",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveClass("img-fluid", {
|
||||
message: "the image should have the correct class",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("width", "90", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveStyle("maxWidth: 90px", {
|
||||
message: "the image should correctly set its attributes",
|
||||
});
|
||||
});
|
||||
|
||||
test("ImageUrlField in subviews are loaded correctly", async () => {
|
||||
PartnerType._fields.image = fields.Char();
|
||||
PartnerType._records[0].image = EN_FLAG_URL;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image_url"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`img[data-src="${FR_FLAG_URL}"]`).toHaveCount(1, {
|
||||
message: "The view's image is in the DOM",
|
||||
});
|
||||
expect(".o_kanban_record:not(.o_kanban_ghost):not(.o-kanban-button-new)").toHaveCount(1, {
|
||||
message: "There should be one record in the many2many",
|
||||
});
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(".o_kanban_record:not(.o_kanban_ghost):not(.o-kanban-button-new)");
|
||||
await animationFrame();
|
||||
expect(".modal").toHaveCount(1, { message: "The modal should have opened" });
|
||||
expect(`img[data-src="${EN_FLAG_URL}"]`).toHaveCount(1, {
|
||||
message: "The dialog's image is in the DOM",
|
||||
});
|
||||
});
|
||||
|
||||
test("image fields in x2many list are loaded correctly", async () => {
|
||||
PartnerType._fields.image = fields.Char();
|
||||
PartnerType._records[0].image = EN_FLAG_URL;
|
||||
Partner._records[0].timmy = [12];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<list>
|
||||
<field name="image" widget="image_url"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("tr.o_data_row").toHaveCount(1, {
|
||||
message: "There should be one record in the many2many",
|
||||
});
|
||||
expect(`img[data-src="${EN_FLAG_URL}"]`).toHaveCount(1, {
|
||||
message: "The list's image is in the DOM",
|
||||
});
|
||||
});
|
||||
|
||||
test("image url fields in kanban don't stop opening record", async () => {
|
||||
patchWithCleanup(KanbanController.prototype, {
|
||||
openRecord() {
|
||||
expect.step("open record");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="foo" widget="image_url"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(".o_kanban_record");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["open record"]);
|
||||
});
|
||||
|
||||
test("image fields with empty value", async () => {
|
||||
Partner._records[0].foo = false;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`div[name="foo"]`).toHaveClass("o_field_image_url", {
|
||||
message: "the widget should have the correct class",
|
||||
});
|
||||
expect(`div[name="foo"] > img`).toHaveCount(0, {
|
||||
message: "the widget should not contain an image",
|
||||
});
|
||||
});
|
||||
|
||||
test("onchange update image fields", async () => {
|
||||
const srcTest = "/my/test/src";
|
||||
Partner._onChanges.name = (record) => {
|
||||
record.foo = srcTest;
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("data-src", FR_FLAG_URL, {
|
||||
message: "the image should have the correct src",
|
||||
});
|
||||
|
||||
await click(`[name="name"] input`);
|
||||
await edit("test", { confirm: "enter" });
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(`div[name="foo"] > img`).toHaveAttribute("data-src", srcTest, {
|
||||
message: "the image should have the onchange src",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
const FR_FLAG_URL = "/base/static/img/country_flags/fr.png";
|
||||
const EN_FLAG_URL = "/base/static/img/country_flags/gb.png";
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char", searchable: true },
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
},
|
||||
timmy: {
|
||||
string: "pokemon",
|
||||
type: "many2many",
|
||||
relation: "partner_type",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
foo: FR_FLAG_URL,
|
||||
timmy: [],
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
partner_type: {
|
||||
fields: {
|
||||
name: { string: "Partner Type", type: "char", searchable: true },
|
||||
color: { string: "Color index", type: "integer", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 12, display_name: "gold", color: 2 },
|
||||
{ id: 14, display_name: "silver", color: 5 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
/**
|
||||
* Same tests than for Image fields, but for Char fields with image_url widget.
|
||||
*/
|
||||
QUnit.module("ImageUrlField");
|
||||
|
||||
QUnit.test("image fields are correctly rendered", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector('div[name="foo"]'),
|
||||
"o_field_image_url",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsOnce(target, 'div[name="foo"] > img', "the widget should contain an image");
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').dataset.src,
|
||||
FR_FLAG_URL,
|
||||
"the image should have the correct src"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector('div[name="foo"] > img'),
|
||||
"img-fluid",
|
||||
"the image should have the correct class"
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector('div[name="foo"] > img'),
|
||||
"width",
|
||||
"90",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').style.maxWidth,
|
||||
"90px",
|
||||
"the image should correctly set its attributes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("ImageUrlField in subviews are loaded correctly", async function (assert) {
|
||||
serverData.models.partner_type.fields.image = { name: "image", type: "char" };
|
||||
serverData.models.partner_type.records[0].image = EN_FLAG_URL;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
<field name="timmy" widget="many2many" mode="kanban">
|
||||
<kanban>
|
||||
<field name="display_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<span><t t-esc="record.display_name.value"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<field name="image" widget="image_url"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${FR_FLAG_URL}"]`),
|
||||
"The view's image is in the DOM"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record .oe_kanban_global_click",
|
||||
"There should be one record in the many2many"
|
||||
);
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await click(target.querySelector(".oe_kanban_global_click"));
|
||||
assert.containsOnce(document.body, ".modal", "The modal should have opened");
|
||||
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
|
||||
"The dialog's image is in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("image fields in x2many list are loaded correctly", async function (assert) {
|
||||
serverData.models.partner_type.fields.image = { name: "image", type: "char" };
|
||||
serverData.models.partner_type.records[0].image = EN_FLAG_URL;
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many">
|
||||
<tree>
|
||||
<field name="image" widget="image_url"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "tr.o_data_row", "There should be one record in the many2many");
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
|
||||
"The list's image is in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("image url fields in kanban don't stop opening record", async function (assert) {
|
||||
patchWithCleanup(KanbanController.prototype, {
|
||||
openRecord() {
|
||||
assert.step("open record");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="foo" widget="image_url"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_kanban_record"));
|
||||
assert.verifySteps(["open record"]);
|
||||
});
|
||||
|
||||
QUnit.test("image fields with empty value", async function (assert) {
|
||||
serverData.models.partner.records[0].foo = false;
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector('div[name="foo"]'),
|
||||
"o_field_image_url",
|
||||
"the widget should have the correct class"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
'div[name="foo"] > img',
|
||||
"the widget should not contain an image"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("onchange update image fields", async function (assert) {
|
||||
const srcTest = "/my/test/src";
|
||||
serverData.models.partner.onchanges = {
|
||||
display_name(record) {
|
||||
record.foo = srcTest;
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field name="foo" widget="image_url" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').dataset.src,
|
||||
FR_FLAG_URL,
|
||||
"the image should have the correct src"
|
||||
);
|
||||
|
||||
await editInput(target, '[name="display_name"] input', "test");
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector('div[name="foo"] > img').dataset.src,
|
||||
srcTest,
|
||||
"the image should have the onchange src"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fieldInput,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Product extends models.Model {
|
||||
price = fields.Integer();
|
||||
}
|
||||
|
||||
defineModels([Product]);
|
||||
|
||||
test("human readable format 1", async () => {
|
||||
Product._records = [{ id: 1, price: 3.756754e6 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'human_readable': 'true'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("4M", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T)",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 2", async () => {
|
||||
Product._records = [{ id: 1, price: 2.034e3 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("2.0k", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T)",
|
||||
});
|
||||
});
|
||||
|
||||
test("human readable format 3", async () => {
|
||||
Product._records = [{ id: 1, price: 6.67543577586e12 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("6.6754T", {
|
||||
message: "The value should be rendered in human readable format (k, M, G, T)",
|
||||
});
|
||||
});
|
||||
|
||||
test("still human readable when readonly", async () => {
|
||||
Product._records = [{ id: 1, price: 6.67543577586e12 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" readonly="true" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget span").toHaveText("6.6754T");
|
||||
});
|
||||
|
||||
test("should be 0 when unset", async () => {
|
||||
Product._records = [{ id: 1 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
expect(".o_field_widget input").not.toHaveClass("o_field_empty");
|
||||
expect(".o_field_widget input").toHaveValue("0");
|
||||
});
|
||||
|
||||
test("basic form view flow", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("10");
|
||||
await fieldInput("price").edit("30");
|
||||
expect(".o_field_widget input").toHaveValue("30");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("30");
|
||||
});
|
||||
|
||||
test("no need to focus out of the input to save the record after correcting an invalid input", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("10");
|
||||
await fieldInput("price").edit("a");
|
||||
expect(".o_field_widget input").toHaveValue("a");
|
||||
expect(".o_form_status_indicator span i.fa-warning").toHaveCount(1);
|
||||
expect(".o_form_button_save[disabled]").toHaveCount(1);
|
||||
await fieldInput("price").edit("1");
|
||||
expect(".o_field_widget input").toHaveValue("1");
|
||||
expect(".o_form_status_indicator span i.fa-warning").toHaveCount(0);
|
||||
expect(".o_form_button_save[disabled]").toHaveCount(0);
|
||||
await clickSave(); // makes sure there is an enabled save button
|
||||
});
|
||||
|
||||
test("rounded when using formula in form view", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
await fieldInput("price").edit("=100/3");
|
||||
expect(".o_field_widget input").toHaveValue("33");
|
||||
});
|
||||
|
||||
test("with input type 'number' option", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'type': 'number'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "number");
|
||||
await fieldInput("price").edit("1234567890");
|
||||
expect(".o_field_widget input").toHaveValue(1234567890, {
|
||||
message: "Integer value must be not formatted if input type is number",
|
||||
});
|
||||
});
|
||||
|
||||
test("with 'step' option", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'type': 'number', 'step': 3}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("step", "3");
|
||||
});
|
||||
|
||||
test("with 'min'/'max' option", async () => {
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'type': 'number', 'min': 3, 'max': 10}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveAttribute("min", "3");
|
||||
expect(".o_field_widget input").toHaveAttribute("max", "10");
|
||||
});
|
||||
|
||||
test("without input type option", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 10 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveAttribute("type", "text");
|
||||
await fieldInput("price").edit("1234567890");
|
||||
expect(".o_field_widget input").toHaveValue("1,234,567,890");
|
||||
});
|
||||
|
||||
test("is formatted by default", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'enable_formatting': 'false'}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
});
|
||||
|
||||
test("basic flow in editable list view", async () => {
|
||||
Product._records = [{ id: 1 }, { id: 2, price: 10 }];
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: '<list editable="bottom"><field name="price"/></list>',
|
||||
});
|
||||
const zeroValues = queryAllTexts("td").filter((text) => text === "0");
|
||||
expect(zeroValues).toHaveLength(1, {
|
||||
message: "Unset integer values should not be rendered as zeros",
|
||||
});
|
||||
await contains("td.o_data_cell").click();
|
||||
expect('.o_field_widget[name="price"] input').toHaveCount(1);
|
||||
await contains('.o_field_widget[name="price"] input').edit("-28");
|
||||
expect("td.o_data_cell:first").toHaveText("-28");
|
||||
expect('.o_field_widget[name="price"] input').toHaveValue("10");
|
||||
await contains(getFixture()).click();
|
||||
expect(queryAllTexts("td.o_data_cell")).toEqual(["-28", "10"]);
|
||||
});
|
||||
|
||||
test("with enable_formatting option as false", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: `<form><field name="price" options="{'enable_formatting': false}"/></form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("8069");
|
||||
await fieldInput("price").edit("1234567890");
|
||||
expect(".o_field_widget input").toHaveValue("1234567890");
|
||||
});
|
||||
|
||||
test("value is formatted on Enter", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("0");
|
||||
|
||||
await fieldInput("price").edit("1000", { confirm: "Enter" });
|
||||
expect(".o_field_widget input").toHaveValue("1,000");
|
||||
});
|
||||
|
||||
test("value is formatted on Enter (even if same value)", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
|
||||
await fieldInput("price").edit("8069", { confirm: "Enter" });
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
});
|
||||
|
||||
test("value is formatted on click out (even if same value)", async () => {
|
||||
// `localization > grouping` required for this test is [3, 0], which is the default in mock server
|
||||
Product._records = [{ id: 1, price: 8069 }];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="price"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
|
||||
await fieldInput("price").edit("8069", { confirm: false });
|
||||
expect(".o_field_widget input").toHaveValue("8069");
|
||||
|
||||
await contains(".o_control_panel").click();
|
||||
expect(".o_field_widget input").toHaveValue("8,069");
|
||||
});
|
||||
|
||||
test("Value should not be a boolean when enable_formatting is false", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "product",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="id" options="{'enable_formatting': false}"/>
|
||||
<field name="price"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
await contains(`.o_list_button_add`).click();
|
||||
expect(".o_selected_row .o_field_integer").toHaveText("");
|
||||
});
|
||||
|
|
@ -1,386 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
findElement,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, int_field: 10 },
|
||||
{ id: 2, int_field: false },
|
||||
{ id: 3, int_field: 8069 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("IntegerField");
|
||||
|
||||
QUnit.test("should be 0 when unset", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_field_widget"),
|
||||
"o_field_empty",
|
||||
"Non-set integer field should be recognized as 0."
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"0",
|
||||
"Non-set integer field should be recognized as 0."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("basic form view flow", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=int_field] input").value,
|
||||
"10",
|
||||
"The value should be rendered correctly in edit mode."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "30");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=int_field] input").value,
|
||||
"30",
|
||||
"The value should be correctly displayed in the input."
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"30",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("rounded when using formula in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "=100/3");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"33",
|
||||
"The new value should be calculated properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with input type 'number' option", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="int_field" options="{'type': 'number'}"/></form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("type"),
|
||||
"Integer field with option type must have a type attribute."
|
||||
);
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"number",
|
||||
'Integer field with option type must have a type attribute equals to "number".'
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "1234567890");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"1234567890",
|
||||
"Integer value must be not formatted if input type is number."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with 'step' option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="int_field" options="{'type': 'number', 'step': 3}"/></form>`,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget input").hasAttribute("step"),
|
||||
"Integer field with option type must have a step attribute."
|
||||
);
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"step",
|
||||
"3",
|
||||
'Integer field with option type must have a step attribute equals to "3".'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("without input type option", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".o_field_widget input"),
|
||||
"type",
|
||||
"text",
|
||||
"Integer field without option type must have a text type (default type)."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "1234567890");
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"1,234,567,890",
|
||||
"Integer value must be formatted if input type isn't number."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with disable formatting option", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
arch: `<form><field name="int_field" options="{'format': 'false'}"/></form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8069",
|
||||
"Integer value must not be formatted"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("IntegerField is formatted by default", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"8,069",
|
||||
"Integer value must be formatted by default"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("basic flow in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="bottom"><field name="int_field"/></tree>',
|
||||
});
|
||||
var zeroValues = Array.from(target.querySelectorAll("td")).filter(
|
||||
(el) => el.textContent === "0"
|
||||
);
|
||||
assert.strictEqual(
|
||||
zeroValues.length,
|
||||
1,
|
||||
"Unset integer values should not be rendered as zeros."
|
||||
);
|
||||
|
||||
// switch to edit mode
|
||||
var cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_field_widget[name="int_field"] input',
|
||||
"The view should have 1 input for editable integer."
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name=int_field] input", "-28");
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_field_widget[name="int_field"] input').value,
|
||||
"-28",
|
||||
"The value should be displayed properly in the input."
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.strictEqual(
|
||||
target.querySelector("td:not(.o_list_record_selector)").textContent,
|
||||
"-28",
|
||||
"The new value should be saved and displayed properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("IntegerField field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form><field name="int_field" placeholder="Placeholder"/></form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='int_field'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='int_field'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"no need to focus out of the input to save the record after correcting an invalid input",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
const input = findElement(target, ".o_field_widget[name=int_field] input");
|
||||
assert.strictEqual(input.value, "10");
|
||||
|
||||
input.value = "a";
|
||||
triggerEvent(input, null, "input", {});
|
||||
assert.strictEqual(input.value, "a");
|
||||
await clickSave(target);
|
||||
assert.containsOnce(target, ".o_form_status_indicator span i.fa-warning");
|
||||
input.value = "1";
|
||||
triggerEvent(input, null, "input", {});
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_form_status_indicator span i.fa-warning");
|
||||
assert.containsOnce(target, ".o_form_button_save");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"make a valid integer field invalid, then reset the original value to make it valid again",
|
||||
async function (assert) {
|
||||
// This test is introduced to fix a bug:
|
||||
// Have a valid value, change it to an invalid value, blur, then change it back to the same valid value.
|
||||
// The field was considered not dirty, so the onChange code wasn't executed, and the model still thought the value was invalid.
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
const fieldSelector = ".o_field_widget[name=int_field]";
|
||||
const inputSelector = fieldSelector + " input";
|
||||
|
||||
assert.strictEqual(target.querySelector(inputSelector).value, "10");
|
||||
|
||||
await editInput(target.querySelector(inputSelector), null, "a");
|
||||
assert.strictEqual(target.querySelector(inputSelector).value, "a");
|
||||
assert.hasClass(target.querySelector(fieldSelector), "o_field_invalid");
|
||||
|
||||
await editInput(target.querySelector(inputSelector), null, "10");
|
||||
assert.strictEqual(target.querySelector(inputSelector).value, "10");
|
||||
assert.doesNotHaveClass(target.querySelector(fieldSelector), "o_field_invalid");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("value is formatted on Enter", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
target.querySelector(".o_field_widget input").value = 1000;
|
||||
await triggerEvent(target, ".o_field_widget input", "input");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "1000");
|
||||
|
||||
await triggerEvent(target, ".o_field_widget input", "keydown", { key: "Enter" });
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "1,000");
|
||||
});
|
||||
|
||||
QUnit.test("value is formatted on Enter (even if same value)", async function (assert) {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 3,
|
||||
arch: '<form><field name="int_field"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
|
||||
|
||||
target.querySelector(".o_field_widget input").value = 8069;
|
||||
await triggerEvent(target, ".o_field_widget input", "input");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8069");
|
||||
|
||||
await triggerEvent(target, ".o_field_widget input", "keydown", { key: "Enter" });
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import { expect, test, describe } from "@odoo/hoot";
|
||||
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
preloadBundle,
|
||||
preventResizeObserverError,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
const INVALID_LOCATOR = ".invalid_locator";
|
||||
|
||||
class IrUiView extends models.Model {
|
||||
_name = "ir.ui.view";
|
||||
_rec_name = "name";
|
||||
|
||||
name = fields.Char({ required: true });
|
||||
arch = fields.Text({});
|
||||
invalid_locators = fields.Json();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Child View",
|
||||
arch: `
|
||||
<data>
|
||||
<xpath expr="//field[@name='name']" position="after"/>
|
||||
<xpath expr="//group" position="inside"/>
|
||||
<xpath expr="//field[@name='inherit_id']" position="replace"/>
|
||||
<xpath expr="//field[@name='non_existent_field']" position="after"/>
|
||||
<xpath expr="//nonexistent_tag" position="inside"/>
|
||||
<xpath expr="//field[@name='arch_invalid']" position="after"/>
|
||||
<field name="invalid" position="replace"/>
|
||||
</data>
|
||||
`,
|
||||
invalid_locators: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([IrUiView]);
|
||||
|
||||
// Preload necessary bundles and prevent ResizeObserver errors
|
||||
preloadBundle("web.ace_lib");
|
||||
preventResizeObserverError();
|
||||
|
||||
const mountChildView = async () =>
|
||||
await mountView({
|
||||
resModel: "ir.ui.view",
|
||||
resId: 1,
|
||||
type: "form",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="invalid_locators"/>
|
||||
<field name="arch" widget="code_ir_ui_view" options='{"mode": "xml"}'/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
describe("Highlight invalid locators in inherited ir.ui.view", () => {
|
||||
test("with no invalid locators", async () => {
|
||||
await mountChildView();
|
||||
expect(INVALID_LOCATOR).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("with invalid locators", async () => {
|
||||
const invalid_locators = [
|
||||
{
|
||||
tag: "xpath",
|
||||
attrib: {
|
||||
expr: "//field[@name='non_existent_field']",
|
||||
position: "after",
|
||||
},
|
||||
sourceline: 6,
|
||||
},
|
||||
{
|
||||
tag: "xpath",
|
||||
attrib: {
|
||||
expr: "//nonexistent_tag",
|
||||
position: "inside",
|
||||
},
|
||||
sourceline: 7,
|
||||
},
|
||||
{
|
||||
tag: "xpath",
|
||||
attrib: {
|
||||
expr: "//field[@name='arch_invalid']",
|
||||
position: "after",
|
||||
},
|
||||
sourceline: 8,
|
||||
},
|
||||
{
|
||||
tag: "field",
|
||||
attrib: {
|
||||
name: "invalid",
|
||||
position: "replace",
|
||||
},
|
||||
sourceline: 9,
|
||||
},
|
||||
];
|
||||
IrUiView._records[0].invalid_locators = invalid_locators;
|
||||
await mountChildView();
|
||||
expect(INVALID_LOCATOR).toHaveCount(invalid_locators.length);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, press } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
findComponent,
|
||||
models,
|
||||
mountView,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
|
||||
const graph_values = [
|
||||
{ value: 300, label: "5-11 Dec" },
|
||||
{ value: 500, label: "12-18 Dec" },
|
||||
{ value: 100, label: "19-25 Dec" },
|
||||
];
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer({
|
||||
string: "int_field",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
});
|
||||
selection = fields.Selection({
|
||||
string: "Selection",
|
||||
searchable: true,
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
graph_data = fields.Text({ string: "Graph Data" });
|
||||
graph_type = fields.Selection({
|
||||
string: "Graph Type",
|
||||
selection: [
|
||||
["line", "Line"],
|
||||
["bar", "Bar"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
int_field: 10,
|
||||
selection: "blocked",
|
||||
graph_type: "bar",
|
||||
graph_data: JSON.stringify([
|
||||
{
|
||||
color: "blue",
|
||||
title: "Partner 1",
|
||||
values: graph_values,
|
||||
key: "A key",
|
||||
area: true,
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
int_field: 0,
|
||||
selection: "normal",
|
||||
graph_type: "line",
|
||||
graph_data: JSON.stringify([
|
||||
{
|
||||
color: "red",
|
||||
title: "Partner 0",
|
||||
values: graph_values,
|
||||
key: "A key",
|
||||
area: true,
|
||||
},
|
||||
]),
|
||||
},
|
||||
];
|
||||
}
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
defineModels([Partner, User]);
|
||||
|
||||
// Kanban
|
||||
// WOWL remove this helper and user the control panel instead
|
||||
const reload = async (kanban, params = {}) => {
|
||||
kanban.env.searchModel.reload(params);
|
||||
kanban.env.searchModel.search();
|
||||
await animationFrame();
|
||||
};
|
||||
|
||||
test.tags("desktop");
|
||||
test("JournalDashboardGraphField is rendered correctly", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(2, {
|
||||
message: "there should be two graphs rendered",
|
||||
});
|
||||
expect(".o_kanban_record:nth-child(1) .o_graph_barchart").toHaveCount(1, {
|
||||
message: "graph of first record should be a barchart",
|
||||
});
|
||||
expect(".o_kanban_record:nth-child(2) .o_graph_linechart").toHaveCount(1, {
|
||||
message: "graph of second record should be a linechart",
|
||||
});
|
||||
|
||||
// reload kanban
|
||||
await click("input.o_searchview_input");
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(2, {
|
||||
message: "there should be two graphs rendered",
|
||||
});
|
||||
});
|
||||
|
||||
test("rendering of a JournalDashboardGraphField in an updated grouped kanban view", async () => {
|
||||
const view = await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
const kanban = findComponent(view, (component) => component instanceof KanbanController);
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(2, {
|
||||
message: "there should be two graph rendered",
|
||||
});
|
||||
await reload(kanban, { groupBy: ["selection"], domain: [["int_field", "=", 10]] });
|
||||
|
||||
expect(".o_dashboard_graph canvas").toHaveCount(1, {
|
||||
message: "there should be one graph rendered",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { fakeCookieService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, getFixture, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
const graph_values = [
|
||||
{ value: 300, label: "5-11 Dec" },
|
||||
{ value: 500, label: "12-18 Dec" },
|
||||
{ value: 100, label: "19-25 Dec" },
|
||||
];
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
selection: {
|
||||
string: "Selection",
|
||||
type: "selection",
|
||||
searchable: true,
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
},
|
||||
graph_data: { string: "Graph Data", type: "text" },
|
||||
graph_type: {
|
||||
string: "Graph Type",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["line", "Line"],
|
||||
["bar", "Bar"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
int_field: 10,
|
||||
selection: "blocked",
|
||||
graph_type: "bar",
|
||||
graph_data: JSON.stringify([
|
||||
{
|
||||
color: "blue",
|
||||
title: "Partner 1",
|
||||
values: graph_values,
|
||||
key: "A key",
|
||||
area: true,
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
int_field: 0,
|
||||
selection: "normal",
|
||||
graph_type: "line",
|
||||
graph_data: JSON.stringify([
|
||||
{
|
||||
color: "red",
|
||||
title: "Partner 0",
|
||||
values: graph_values,
|
||||
key: "A key",
|
||||
area: true,
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("cookie", fakeCookieService);
|
||||
});
|
||||
|
||||
async function reloadKanbanView(target) {
|
||||
await click(target, "input.o_searchview_input");
|
||||
await triggerEvent(target, "input.o_searchview_input", "keydown", { key: "Enter" });
|
||||
}
|
||||
// Kanban
|
||||
// WOWL remove this helper and user the control panel instead
|
||||
const reload = async (kanban, params = {}) => {
|
||||
kanban.env.searchModel.reload(params);
|
||||
kanban.env.searchModel.search();
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
QUnit.module("JournalDashboardGraphField");
|
||||
|
||||
QUnit.test("JournalDashboardGraphField is rendered correctly", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
2,
|
||||
"there should be two graphs rendered"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(1) .o_graph_barchart",
|
||||
"graph of first record should be a barchart"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(2) .o_graph_linechart",
|
||||
"graph of second record should be a linechart"
|
||||
);
|
||||
|
||||
await reloadKanbanView(target);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
2,
|
||||
"there should be two graphs rendered"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"rendering of a JournalDashboardGraphField in an updated grouped kanban view",
|
||||
async function (assert) {
|
||||
const kanban = await makeView({
|
||||
serverData,
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<field name="graph_type"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
domain: [["id", "in", [1, 2]]],
|
||||
});
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
2,
|
||||
"there should be two graph rendered"
|
||||
);
|
||||
|
||||
await reload(kanban, { groupBy: ["selection"], domain: [["int_field", "=", 10]] });
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_dashboard_graph canvas",
|
||||
"there should be one graph rendered"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Product extends models.Model {
|
||||
json_field = fields.Json();
|
||||
|
||||
_records = [{ id: 1, json_field: "['coupon', 'promotion']" }];
|
||||
}
|
||||
|
||||
defineModels([Product]);
|
||||
|
||||
test("basic rendering", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: '<form><field name="json_field"/></form>',
|
||||
});
|
||||
expect(".o_field_json").toHaveCount(1);
|
||||
expect(".o_field_json span").toHaveText(`"['coupon', 'promotion']"`);
|
||||
});
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char();
|
||||
selection = fields.Selection({
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
});
|
||||
_records = [
|
||||
{
|
||||
foo: "yop",
|
||||
selection: "blocked",
|
||||
},
|
||||
{
|
||||
foo: "blip",
|
||||
selection: "normal",
|
||||
},
|
||||
{
|
||||
foo: "abc",
|
||||
selection: "done",
|
||||
},
|
||||
];
|
||||
}
|
||||
defineModels([Partner]);
|
||||
|
||||
test("LabelSelectionField in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have a warning status label since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(0, {
|
||||
message: "should not have a default status since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(0, {
|
||||
message: "should not have a success status since selection is the second, blocked state",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the label should say 'Blocked' since this is the label value for that state",
|
||||
});
|
||||
});
|
||||
|
||||
test("LabelSelectionField in editable list view", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<list editable="bottom">
|
||||
<field name="foo"/>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget .badge:not(:empty)").toHaveCount(3, {
|
||||
message: "should have three visible status labels",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have one warning status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the warning label should read 'Blocked'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(1, {
|
||||
message: "should have one default status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveText("Normal", {
|
||||
message: "the default label should read 'Normal'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(1, {
|
||||
message: "should have one success status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveText("Done", {
|
||||
message: "the success label should read 'Done'",
|
||||
});
|
||||
|
||||
// switch to edit mode and check the result
|
||||
await click("tbody td:not(.o_list_record_selector)");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_field_widget .badge:not(:empty)").toHaveCount(3, {
|
||||
message: "should have three visible status labels",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have one warning status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the warning label should read 'Blocked'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(1, {
|
||||
message: "should have one default status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveText("Normal", {
|
||||
message: "the default label should read 'Normal'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(1, {
|
||||
message: "should have one success status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveText("Done", {
|
||||
message: "the success label should read 'Done'",
|
||||
});
|
||||
|
||||
// save and check the result
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
expect(".o_field_widget .badge:not(:empty)").toHaveCount(3, {
|
||||
message: "should have three visible status labels",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveCount(1, {
|
||||
message: "should have one warning status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-warning").toHaveText("Blocked", {
|
||||
message: "the warning label should read 'Blocked'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveCount(1, {
|
||||
message: "should have one default status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-secondary").toHaveText("Normal", {
|
||||
message: "the default label should read 'Normal'",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveCount(1, {
|
||||
message: "should have one success status label",
|
||||
});
|
||||
expect(".o_field_widget .badge.text-bg-success").toHaveText("Done", {
|
||||
message: "the success label should read 'Done'",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
selection: {
|
||||
string: "Selection",
|
||||
type: "selection",
|
||||
searchable: true,
|
||||
selection: [
|
||||
["normal", "Normal"],
|
||||
["blocked", "Blocked"],
|
||||
["done", "Done"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
foo: "yop",
|
||||
selection: "blocked",
|
||||
},
|
||||
{
|
||||
foo: "blip",
|
||||
selection: "normal",
|
||||
},
|
||||
{
|
||||
foo: "abc",
|
||||
selection: "done",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("LabelSelectionField");
|
||||
|
||||
QUnit.test("LabelSelectionField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have a warning status label since selection is the second, blocked state"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should not have a default status since selection is the second, blocked state"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should not have a success status since selection is the second, blocked state"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the label should say 'Blocked' since this is the label value for that state"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("LabelSelectionField in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="foo"/>
|
||||
<field name="selection" widget="label_selection"
|
||||
options="{'classes': {'normal': 'secondary', 'blocked': 'warning','done': 'success'}}"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
|
||||
3,
|
||||
"should have three visible status labels"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have one warning status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the warning label should read 'Blocked'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should have one default status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-secondary").textContent,
|
||||
"Normal",
|
||||
"the default label should read 'Normal'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should have one success status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-success").textContent,
|
||||
"Done",
|
||||
"the success label should read 'Done'"
|
||||
);
|
||||
|
||||
// switch to edit mode and check the result
|
||||
await click(target.querySelector("tbody td:not(.o_list_record_selector)"));
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
|
||||
3,
|
||||
"should have three visible status labels"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have one warning status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the warning label should read 'Blocked'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should have one default status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-secondary").textContent,
|
||||
"Normal",
|
||||
"the default label should read 'Normal'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should have one success status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-success").textContent,
|
||||
"Done",
|
||||
"the success label should read 'Done'"
|
||||
);
|
||||
|
||||
// save and check the result
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
|
||||
3,
|
||||
"should have three visible status labels"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-warning",
|
||||
"should have one warning status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-warning").textContent,
|
||||
"Blocked",
|
||||
"the warning label should read 'Blocked'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-secondary",
|
||||
"should have one default status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-secondary").textContent,
|
||||
"Normal",
|
||||
"the default label should read 'Normal'"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget .badge.text-bg-success",
|
||||
"should have one success status label"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget .badge.text-bg-success").textContent,
|
||||
"Done",
|
||||
"the success label should read 'Done'"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { setInputFiles } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
MockServer,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Turtle extends models.Model {
|
||||
picture_ids = fields.Many2many({
|
||||
string: "Pictures",
|
||||
relation: "ir.attachment",
|
||||
});
|
||||
_records = [{ id: 1, picture_ids: [17] }];
|
||||
}
|
||||
|
||||
class IrAttachment extends models.Model {
|
||||
_name = "ir.attachment";
|
||||
name = fields.Char();
|
||||
mimetype = fields.Char();
|
||||
_records = [{ id: 17, name: "Marley&Me.jpg", mimetype: "jpg" }];
|
||||
}
|
||||
|
||||
defineModels([Turtle, IrAttachment]);
|
||||
|
||||
test("widget many2many_binary", async () => {
|
||||
expect.assertions(17);
|
||||
|
||||
mockService("http", {
|
||||
post(route, { ufile }) {
|
||||
expect(route).toBe("/web/binary/upload_attachment");
|
||||
expect(ufile[0].name).toBe("fake_file.tiff", {
|
||||
message: "file is correctly uploaded to the server",
|
||||
});
|
||||
const ids = MockServer.env["ir.attachment"].create(
|
||||
ufile.map(({ name }) => ({ name, mimetype: "text/plain" }))
|
||||
);
|
||||
return JSON.stringify(MockServer.env["ir.attachment"].read(ids));
|
||||
},
|
||||
});
|
||||
|
||||
IrAttachment._views.list = '<list string="Pictures"><field name="name"/></list>';
|
||||
onRpc((args) => {
|
||||
if (args.method !== "get_views") {
|
||||
expect.step(args.route);
|
||||
}
|
||||
if (args.method === "web_read" && args.model === "turtle") {
|
||||
expect(args.kwargs.specification).toEqual({
|
||||
display_name: {},
|
||||
picture_ids: {
|
||||
fields: {
|
||||
mimetype: {},
|
||||
name: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (args.method === "web_save" && args.model === "turtle") {
|
||||
expect(args.kwargs.specification).toEqual({
|
||||
display_name: {},
|
||||
picture_ids: {
|
||||
fields: {
|
||||
mimetype: {},
|
||||
name: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (args.method === "web_read" && args.model === "ir.attachment") {
|
||||
expect(args.kwargs.specification).toEqual({
|
||||
mimetype: {},
|
||||
name: {},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .oe_fileupload").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attach").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attach").toHaveText("Pictures");
|
||||
|
||||
expect("input.o_input_file").toHaveAttribute("accept", "image/*");
|
||||
expect.verifySteps(["/web/dataset/call_kw/turtle/web_read"]);
|
||||
|
||||
// Set and trigger the change of a file for the input
|
||||
await contains(".o_file_input_trigger").click();
|
||||
await setInputFiles(new File(["fake_file"], "fake_file.tiff", { type: "text/plain" }));
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_attachment:nth-child(2) .caption a:eq(0)").toHaveText("fake_file.tiff", {
|
||||
message: 'value of attachment should be "fake_file.tiff"',
|
||||
});
|
||||
expect(".o_attachment:nth-child(2) .caption.small a").toHaveText("TIFF", {
|
||||
message: "file extension should be correct",
|
||||
});
|
||||
expect(".o_attachment:nth-child(2) .o_image.o_hover").toHaveAttribute(
|
||||
"data-mimetype",
|
||||
"text/plain",
|
||||
{ message: "preview displays the right mimetype" }
|
||||
);
|
||||
|
||||
// delete the attachment
|
||||
await contains("div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete").click();
|
||||
|
||||
await clickSave();
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect.verifySteps([
|
||||
"/web/dataset/call_kw/ir.attachment/web_read",
|
||||
"/web/dataset/call_kw/turtle/web_save",
|
||||
]);
|
||||
});
|
||||
|
||||
test("widget many2many_binary displays notification on error", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
mockService("http", {
|
||||
post(route, { ufile }) {
|
||||
expect(route).toBe("/web/binary/upload_attachment");
|
||||
expect([ufile[0].name, ufile[1].name]).toEqual(["good_file.txt", "bad_file.txt"], {
|
||||
message: "files are correctly sent to the server",
|
||||
});
|
||||
const ids = MockServer.env["ir.attachment"].create({
|
||||
name: ufile[0].name,
|
||||
mimetype: "text/plain",
|
||||
});
|
||||
return JSON.stringify([
|
||||
...MockServer.env["ir.attachment"].read(ids),
|
||||
{
|
||||
name: ufile[1].name,
|
||||
mimetype: "text/plain",
|
||||
error: `Error on file: ${ufile[1].name}`,
|
||||
},
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
IrAttachment._views.list = '<list string="Pictures"><field name="name"/></list>';
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .oe_fileupload").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attach").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete").toHaveCount(1);
|
||||
|
||||
// Set and trigger the import of 2 files in the input
|
||||
await contains(".o_file_input_trigger").click();
|
||||
await setInputFiles([
|
||||
new File(["good_file"], "good_file.txt", { type: "text/plain" }),
|
||||
new File(["bad_file"], "bad_file.txt", { type: "text/plain" }),
|
||||
]);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_attachment:nth-child(2) .caption a:eq(0)").toHaveText("good_file.txt", {
|
||||
message: 'value of attachment should be "good_file.txt"',
|
||||
});
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_content").toHaveText("Uploading error. Error on file: bad_file.txt");
|
||||
expect(".o_notification_bar").toHaveClass("bg-danger");
|
||||
});
|
||||
|
||||
test("widget many2many_binary image MIME type preview", async () => {
|
||||
expect.assertions(9);
|
||||
|
||||
const IMAGE_B64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z9DwHwAGBQKA3H7sNwAAAABJRU5ErkJggg==";
|
||||
const imageData = Uint8Array.from([...atob(IMAGE_B64)].map((c) => c.charCodeAt(0)));
|
||||
|
||||
mockService("http", {
|
||||
post(route, { ufile }) {
|
||||
expect(route).toBe("/web/binary/upload_attachment");
|
||||
expect(ufile[0].name).toBe("fake_image.png", {
|
||||
message: "file is correctly uploaded to the server",
|
||||
});
|
||||
const ids = MockServer.env["ir.attachment"].create(
|
||||
ufile.map(({ name }) => ({ name, mimetype: "image/png" }))
|
||||
);
|
||||
return JSON.stringify(MockServer.env["ir.attachment"].read(ids));
|
||||
},
|
||||
});
|
||||
|
||||
IrAttachment._views.list = '<list string="Pictures"><field name="name"/></list>';
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget .oe_fileupload").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachments").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attach").toHaveCount(1);
|
||||
expect("div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete").toHaveCount(1);
|
||||
|
||||
// Set and trigger the import of a png image in the input
|
||||
await contains(".o_file_input_trigger").click();
|
||||
await setInputFiles(new File([imageData], "fake_image.png", { type: "image/png" }));
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_attachment:nth-child(2) .caption a:eq(0)").toHaveText("fake_image.png", {
|
||||
message: 'value of attachment should be "fake_image.png"',
|
||||
});
|
||||
expect(".o_attachment:nth-child(2) .caption.small a").toHaveText("PNG", {
|
||||
message: "file extension should be correct",
|
||||
});
|
||||
expect(".o_attachment:nth-child(2) .o_preview_image.o_hover").toHaveAttribute(
|
||||
"src",
|
||||
`data:image/png;base64,${IMAGE_B64}`,
|
||||
{ message: "preview should display the image preview" }
|
||||
);
|
||||
});
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
turtle: {
|
||||
fields: {
|
||||
picture_ids: {
|
||||
string: "Pictures",
|
||||
type: "many2many",
|
||||
relation: "ir.attachment",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
picture_ids: [17],
|
||||
},
|
||||
],
|
||||
},
|
||||
"ir.attachment": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
mimetype: { string: "Mimetype", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 17,
|
||||
name: "Marley&Me.jpg",
|
||||
mimetype: "jpg",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2ManyBinaryField");
|
||||
|
||||
QUnit.test("widget many2many_binary", async function (assert) {
|
||||
assert.expect(24);
|
||||
|
||||
const fakeHTTPService = {
|
||||
start() {
|
||||
return {
|
||||
post: (route, params) => {
|
||||
assert.strictEqual(route, "/web/binary/upload_attachment");
|
||||
assert.strictEqual(
|
||||
params.ufile[0].name,
|
||||
"fake_file.tiff",
|
||||
"file is correctly uploaded to the server"
|
||||
);
|
||||
const file = {
|
||||
id: 10,
|
||||
name: params.ufile[0].name,
|
||||
mimetype: "text/plain",
|
||||
};
|
||||
serverData.models["ir.attachment"].records.push(file);
|
||||
return JSON.stringify([file]);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("http", fakeHTTPService);
|
||||
|
||||
serverData.views = {
|
||||
"ir.attachment,false,list": '<tree string="Pictures"><field name="name"/></tree>',
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
mockRPC(route, args) {
|
||||
if (args.method !== "get_views") {
|
||||
assert.step(route);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/ir.attachment/read") {
|
||||
assert.deepEqual(args.args[1], ["name", "mimetype"]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload",
|
||||
"there should be the attachment widget"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be one attachment"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attach",
|
||||
"there should be an Add button (edit)"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete",
|
||||
"there should be a Delete button (edit)"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attach",
|
||||
"there should be an Add button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("div.o_field_widget .oe_fileupload .o_attach").textContent.trim(),
|
||||
"Pictures",
|
||||
"the button should be correctly named"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input_file").getAttribute("accept"),
|
||||
"image/*",
|
||||
'there should be an attribute "accept" on the input'
|
||||
);
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/turtle/read",
|
||||
"/web/dataset/call_kw/ir.attachment/read",
|
||||
]);
|
||||
|
||||
// Set and trigger the change of a file for the input
|
||||
const fileInput = target.querySelector('input[type="file"]');
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(new File(["fake_file"], "fake_file.tiff", { type: "text/plain" }));
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .caption a").textContent,
|
||||
"fake_file.tiff",
|
||||
'value of attachment should be "fake_file.tiff"'
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .caption.small a").textContent,
|
||||
"tiff",
|
||||
"file extension should be correct"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .o_image.o_hover").dataset.mimetype,
|
||||
"text/plain",
|
||||
"preview displays the right mimetype"
|
||||
);
|
||||
|
||||
// delete the attachment
|
||||
await click(
|
||||
target.querySelector(
|
||||
"div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete"
|
||||
)
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be only one attachment left"
|
||||
);
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/ir.attachment/read",
|
||||
"/web/dataset/call_kw/turtle/write",
|
||||
"/web/dataset/call_kw/turtle/read",
|
||||
"/web/dataset/call_kw/ir.attachment/read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2many_binary displays notification on error", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
const fakeHTTPService = {
|
||||
start() {
|
||||
return {
|
||||
post: (route, params) => {
|
||||
assert.strictEqual(route, "/web/binary/upload_attachment");
|
||||
assert.deepEqual(
|
||||
[params.ufile[0].name, params.ufile[1].name],
|
||||
["good_file.txt", "bad_file.txt"],
|
||||
"files are correctly sent to the server"
|
||||
);
|
||||
const files = [
|
||||
{
|
||||
id: 10,
|
||||
name: params.ufile[0].name,
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: params.ufile[1].name,
|
||||
mimetype: "text/plain",
|
||||
error: `Error on file: ${params.ufile[1].name}`,
|
||||
},
|
||||
];
|
||||
serverData.models["ir.attachment"].records.push(files[0]);
|
||||
return JSON.stringify(files);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("http", fakeHTTPService);
|
||||
|
||||
serverData.views = {
|
||||
"ir.attachment,false,list": '<tree string="Pictures"><field name="name"/></tree>',
|
||||
};
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="picture_ids" widget="many2many_binary" options="{'accepted_file_extensions': 'image/*'}"/>
|
||||
</group>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload",
|
||||
"there should be the attachment widget"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be one attachment"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attach",
|
||||
"there should be an Add button (edit)"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachment .o_attachment_delete",
|
||||
"there should be a Delete button (edit)"
|
||||
);
|
||||
|
||||
// Set and trigger the import of 2 files in the input
|
||||
const fileInput = target.querySelector('input[type="file"]');
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(new File(["good_file"], "good_file.txt", { type: "text/plain" }));
|
||||
dataTransfer.items.add(new File(["bad_file"], "bad_file.txt", { type: "text/plain" }));
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_attachment:nth-child(2) .caption a").textContent,
|
||||
"good_file.txt",
|
||||
'value of attachment should be "good_file.txt"'
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_field_widget .oe_fileupload .o_attachments",
|
||||
"there should be only one attachment uploaded"
|
||||
);
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_title").textContent,
|
||||
"Uploading error"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notification_content").textContent,
|
||||
"Error on file: bad_file.txt"
|
||||
);
|
||||
assert.hasClass(target.querySelector(".o_notification"), "border-danger");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,496 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer({ sortable: true });
|
||||
timmy = fields.Many2many({ string: "pokemon", relation: "partner.type" });
|
||||
p = fields.One2many({
|
||||
string: "one2many field",
|
||||
relation: "partner",
|
||||
relation_field: "trululu",
|
||||
});
|
||||
trululu = fields.Many2one({ relation: "partner" });
|
||||
_records = [{ id: 1, int_field: 10, p: [1] }];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
name = fields.Char();
|
||||
_records = [
|
||||
{ id: 12, name: "gold" },
|
||||
{ id: 14, name: "silver" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
test("Many2ManyCheckBoxesField", async () => {
|
||||
Partner._records[0].timmy = [12];
|
||||
const commands = [[[4, 14]], [[3, 12]]];
|
||||
onRpc("web_save", (args) => {
|
||||
expect.step("web_save");
|
||||
expect(args.args[1].timmy).toEqual(commands.shift());
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
|
||||
expect("div.o_field_widget div.form-check input:disabled").toHaveCount(0);
|
||||
|
||||
// add a m2m value by clicking on input
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
|
||||
// remove a m2m value by clinking on label
|
||||
await contains("div.o_field_widget div.form-check > label").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").not.toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").toBeChecked();
|
||||
|
||||
expect.verifySteps(["web_save", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField (readonly)", async () => {
|
||||
Partner._records[0].timmy = [12];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" readonly="True" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2, {
|
||||
message: "should have fetched and displayed the 2 values of the many2many",
|
||||
});
|
||||
expect("div.o_field_widget div.form-check input:disabled").toHaveCount(2, {
|
||||
message: "the checkboxes should be disabled",
|
||||
});
|
||||
|
||||
await contains("div.o_field_widget div.form-check > label:eq(1)").click();
|
||||
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField does not read added record", async () => {
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc((args) => {
|
||||
expect.step(args.method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input").click();
|
||||
await runAllTimers();
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(1);
|
||||
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(1);
|
||||
|
||||
expect.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField: start non empty, then remove twice", async () => {
|
||||
Partner._records[0].timmy = [12, 14];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
await runAllTimers();
|
||||
await clickSave();
|
||||
expect("div.o_field_widget div.form-check input:eq(0)").not.toBeChecked();
|
||||
expect("div.o_field_widget div.form-check input:eq(1)").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField: many2many read, field context is properly sent", async () => {
|
||||
onRpc((args) => {
|
||||
expect.step(args.method);
|
||||
if (args.method === "web_read") {
|
||||
expect(args.kwargs.specification.timmy.context).toEqual({ hello: "world" });
|
||||
} else if (args.method === "name_search") {
|
||||
expect(args.kwargs.context.hello).toEqual("world");
|
||||
}
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" context="{ 'hello': 'world' }" />
|
||||
</form>`,
|
||||
});
|
||||
expect.verifySteps(["get_views", "web_read", "name_search"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField: values are updated when domain changes", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" />
|
||||
<field name="timmy" widget="many2many_checkboxes" domain="[['id', '>', int_field]]" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='int_field'] input").toHaveValue("10");
|
||||
expect(".o_field_widget[name='timmy'] .form-check").toHaveCount(2);
|
||||
expect(".o_field_widget[name='timmy']").toHaveText("gold\nsilver");
|
||||
|
||||
await contains(".o_field_widget[name='int_field'] input").edit(13);
|
||||
expect(".o_field_widget[name='timmy'] .form-check").toHaveCount(1);
|
||||
expect(".o_field_widget[name='timmy']").toHaveText("silver");
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField with 40+ values", async () => {
|
||||
// 40 is the default limit for x2many fields. However, the many2many_checkboxes is a
|
||||
// special field that fetches its data through the fetchSpecialData mechanism, and it
|
||||
// uses the name_search server-side limit of 100. This test comes with a fix for a bug
|
||||
// that occurred when the user (un)selected a checkbox that wasn't in the 40 first checkboxes,
|
||||
// because the piece of data corresponding to that checkbox hadn't been processed by the
|
||||
// BasicModel, whereas the code handling the change assumed it had.
|
||||
expect.assertions(3);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id <= 90; id++) {
|
||||
records.push({
|
||||
id,
|
||||
name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
PartnerType._records = records;
|
||||
Partner._records[0].timmy = records.map((r) => r.id);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].timmy).toEqual([[3, records[records.length - 1].id]]);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']:checked").toHaveCount(90);
|
||||
|
||||
// toggle the last value
|
||||
await contains(".o_field_widget[name='timmy'] input[type='checkbox']:last").click();
|
||||
await runAllTimers();
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']:last").not.toBeChecked();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField with 100+ values", async () => {
|
||||
// The many2many_checkboxes widget limits the displayed values to 100 (this is the
|
||||
// server-side name_search limit). This test encodes a scenario where there are more than
|
||||
// 100 records in the co-model, and all values in the many2many relationship aren't
|
||||
// displayed in the widget (due to the limit). If the user (un)selects a checkbox, we don't
|
||||
// want to remove all values that aren't displayed from the relation.
|
||||
expect.assertions(5);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id < 150; id++) {
|
||||
records.push({
|
||||
id,
|
||||
name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
PartnerType._records = records;
|
||||
Partner._records[0].timmy = records.map((r) => r.id);
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].timmy).toEqual([[3, records[0].id]]);
|
||||
expect.step("web_save");
|
||||
});
|
||||
onRpc("name_search", () => {
|
||||
expect.step("name_search");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']").toHaveCount(100);
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']").toBeChecked();
|
||||
|
||||
// toggle the first value
|
||||
await contains(".o_field_widget[name='timmy'] input[type='checkbox']").click();
|
||||
await runAllTimers();
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget[name='timmy'] input[type='checkbox']:first").not.toBeChecked();
|
||||
expect.verifySteps(["name_search", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField in a one2many", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
PartnerType._records.push({ id: 15, name: "bronze" });
|
||||
Partner._records[0].timmy = [14, 15];
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({
|
||||
p: [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
{
|
||||
timmy: [
|
||||
[4, 12],
|
||||
[3, 14],
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<list><field name="id"/></list>
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await contains(".o_data_cell").click();
|
||||
|
||||
// edit the timmy field by (un)checking boxes on the widget
|
||||
await contains(".modal .form-check-input:eq(0)").click();
|
||||
expect(".modal .form-check-input:eq(0)").toBeChecked();
|
||||
await contains(".modal .form-check-input:eq(1)").click();
|
||||
expect(".modal .form-check-input:eq(1)").not.toBeChecked();
|
||||
|
||||
await contains(".modal .o_form_button_save").click();
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField with default values", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
Partner._fields.timmy = fields.Many2many({
|
||||
string: "pokemon",
|
||||
relation: "partner.type",
|
||||
default: [[4, 3]],
|
||||
});
|
||||
PartnerType._records.push({ id: 3, name: "bronze" });
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].timmy).toEqual([[4, 12]], {
|
||||
message: "correct values should have been sent to create",
|
||||
});
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_form_view .form-check input:eq(0)").not.toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(1)").not.toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(2)").toBeChecked();
|
||||
|
||||
await contains(".o_form_view .form-check input:checked").click();
|
||||
await contains(".o_form_view .form-check input:eq(0)").click();
|
||||
await contains(".o_form_view .form-check input:eq(0)").click();
|
||||
await contains(".o_form_view .form-check input:eq(0)").click();
|
||||
await runAllTimers();
|
||||
|
||||
expect(".o_form_view .form-check input:eq(0)").toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(1)").not.toBeChecked();
|
||||
expect(".o_form_view .form-check input:eq(2)").not.toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField batches successive changes", async () => {
|
||||
Partner._fields.timmy = fields.Many2many({
|
||||
string: "pokemon",
|
||||
relation: "partner.type",
|
||||
onChange: () => {},
|
||||
});
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
// checkboxes are updated directly
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
// but no onchanges has been fired yet
|
||||
expect.verifySteps(["get_views", "web_read", "name_search"]);
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["onchange"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField sends batched changes on save", async () => {
|
||||
Partner._fields.timmy = fields.Many2many({
|
||||
string: "pokemon",
|
||||
relation: "partner.type",
|
||||
onChange: () => {},
|
||||
});
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
// checkboxes are updated directly
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
// but no onchanges has been fired yet
|
||||
expect.verifySteps(["get_views", "web_read", "name_search"]);
|
||||
await runAllTimers();
|
||||
// save
|
||||
await clickSave();
|
||||
expect.verifySteps(["onchange", "web_save"]);
|
||||
});
|
||||
|
||||
test("Many2ManyCheckBoxesField in a notebook tab", async () => {
|
||||
Partner._records[0].timmy = [];
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<notebook>
|
||||
<page string="Page 1">
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</page>
|
||||
<page string="Page 2">
|
||||
<field name="int_field" />
|
||||
</page>
|
||||
</notebook>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect("div.o_field_widget[name=timmy]").toHaveCount(1);
|
||||
expect("div.o_field_widget[name=timmy] div.form-check").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_field_widget .form-check-label")).toEqual(["gold", "silver"]);
|
||||
expect("div.o_field_widget[name=timmy] div.form-check input:checked").toHaveCount(0);
|
||||
|
||||
await contains("div.o_field_widget div.form-check input:eq(0)").click();
|
||||
await contains("div.o_field_widget div.form-check input:eq(1)").click();
|
||||
// checkboxes are updated directly
|
||||
expect("div.o_field_widget div.form-check input:checked").toHaveCount(2);
|
||||
// go to the other tab
|
||||
await contains(".o_notebook .nav-link:eq(1)").click();
|
||||
expect("div.o_field_widget[name=timmy]").toHaveCount(0);
|
||||
expect("div.o_field_widget[name=int_field]").toHaveCount(1);
|
||||
// save
|
||||
await clickSave();
|
||||
expect.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
|
||||
});
|
||||
|
|
@ -1,411 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: { string: "int_field", type: "integer", sortable: true },
|
||||
timmy: { string: "pokemon", type: "many2many", relation: "partner_type" },
|
||||
p: {
|
||||
string: "one2many field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "trululu",
|
||||
},
|
||||
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
|
||||
},
|
||||
records: [{ id: 1, int_field: 10, p: [1] }],
|
||||
onchanges: {},
|
||||
},
|
||||
partner_type: {
|
||||
records: [
|
||||
{ id: 12, display_name: "gold" },
|
||||
{ id: 14, display_name: "silver" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2ManyCheckBoxesField");
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField", async function (assert) {
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
const commands = [[[6, false, [12, 14]]], [[6, false, [14]]]];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write") {
|
||||
assert.step("write");
|
||||
assert.deepEqual(args.args[1].timmy, commands.shift());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(target, "div.o_field_widget div.form-check", 2);
|
||||
|
||||
let checkboxes = target.querySelectorAll("div.o_field_widget div.form-check input");
|
||||
assert.ok(checkboxes[0].checked);
|
||||
assert.notOk(checkboxes[1].checked);
|
||||
|
||||
assert.containsNone(target, "div.o_field_widget div.form-check input:disabled");
|
||||
|
||||
// add a m2m value by clicking on input
|
||||
checkboxes = target.querySelectorAll("div.o_field_widget div.form-check input");
|
||||
await click(checkboxes[1]);
|
||||
await clickSave(target);
|
||||
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
|
||||
|
||||
// remove a m2m value by clinking on label
|
||||
await click(target.querySelector("div.o_field_widget div.form-check > label"));
|
||||
await clickSave(target);
|
||||
checkboxes = target.querySelectorAll("div.o_field_widget div.form-check input");
|
||||
assert.notOk(checkboxes[0].checked);
|
||||
assert.ok(checkboxes[1].checked);
|
||||
|
||||
assert.verifySteps(["write", "write"]);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField (readonly)", async function (assert) {
|
||||
serverData.models.partner.records[0].timmy = [12];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" attrs="{'readonly': true}" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
"div.o_field_widget div.form-check",
|
||||
2,
|
||||
"should have fetched and displayed the 2 values of the many2many"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"div.o_field_widget div.form-check input:disabled",
|
||||
2,
|
||||
"the checkboxes should be disabled"
|
||||
);
|
||||
|
||||
await click(target.querySelectorAll("div.o_field_widget div.form-check > label")[1]);
|
||||
|
||||
assert.ok(
|
||||
target.querySelector("div.o_field_widget div.form-check input").checked,
|
||||
"first checkbox should be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll("div.o_field_widget div.form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Many2ManyCheckBoxesField: start non empty, then remove twice",
|
||||
async function (assert) {
|
||||
serverData.models.partner.records[0].timmy = [12, 14];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<group>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</group>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll("div.o_field_widget div.form-check input")[0]);
|
||||
await click(target.querySelectorAll("div.o_field_widget div.form-check input")[1]);
|
||||
await clickSave(target);
|
||||
assert.notOk(
|
||||
target.querySelectorAll("div.o_field_widget div.form-check input")[0].checked,
|
||||
"first checkbox should not be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll("div.o_field_widget div.form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Many2ManyCheckBoxesField: values are updated when domain changes",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field" />
|
||||
<field name="timmy" widget="many2many_checkboxes" domain="[['id', '>', int_field]]" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='int_field'] input").value,
|
||||
"10"
|
||||
);
|
||||
assert.containsN(target, ".o_field_widget[name='timmy'] .form-check", 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='timmy']").textContent,
|
||||
"goldsilver"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_field_widget[name='int_field'] input", 13);
|
||||
assert.containsOnce(target, ".o_field_widget[name='timmy'] .form-check");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='timmy']").textContent,
|
||||
"silver"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField with 40+ values", async function (assert) {
|
||||
// 40 is the default limit for x2many fields. However, the many2many_checkboxes is a
|
||||
// special field that fetches its data through the fetchSpecialData mechanism, and it
|
||||
// uses the name_search server-side limit of 100. This test comes with a fix for a bug
|
||||
// that occurred when the user (un)selected a checkbox that wasn't in the 40 first checkboxes,
|
||||
// because the piece of data corresponding to that checkbox hadn't been processed by the
|
||||
// BasicModel, whereas the code handling the change assumed it had.
|
||||
assert.expect(3);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id <= 90; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner_type.records = records;
|
||||
serverData.models.partner.records[0].timmy = records.map((r) => r.id);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
const expectedIds = records.map((r) => r.id);
|
||||
expectedIds.pop();
|
||||
assert.deepEqual(args[1].timmy, [[6, false, expectedIds]]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']:checked",
|
||||
90
|
||||
);
|
||||
|
||||
// toggle the last value
|
||||
let checkboxes = target.querySelectorAll(
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']"
|
||||
);
|
||||
await click(checkboxes[checkboxes.length - 1]);
|
||||
|
||||
await clickSave(target);
|
||||
checkboxes = target.querySelectorAll(
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']"
|
||||
);
|
||||
assert.notOk(checkboxes[checkboxes.length - 1].checked);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField with 100+ values", async function (assert) {
|
||||
// The many2many_checkboxes widget limits the displayed values to 100 (this is the
|
||||
// server-side name_search limit). This test encodes a scenario where there are more than
|
||||
// 100 records in the co-model, and all values in the many2many relationship aren't
|
||||
// displayed in the widget (due to the limit). If the user (un)selects a checkbox, we don't
|
||||
// want to remove all values that aren't displayed from the relation.
|
||||
assert.expect(7);
|
||||
|
||||
const records = [];
|
||||
for (let id = 1; id < 150; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `type ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner_type.records = records;
|
||||
serverData.models.partner.records[0].timmy = records.map((r) => r.id);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes" />
|
||||
</form>`,
|
||||
async mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
const expectedIds = records.map((r) => r.id);
|
||||
expectedIds.shift();
|
||||
assert.deepEqual(args[1].timmy, [[6, false, expectedIds]]);
|
||||
assert.step("write");
|
||||
}
|
||||
if (method === "name_search") {
|
||||
assert.step("name_search");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_widget[name='timmy'] input[type='checkbox']",
|
||||
100,
|
||||
"should only display 100 checkboxes"
|
||||
);
|
||||
assert.ok(
|
||||
target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']").checked
|
||||
);
|
||||
|
||||
// toggle the first value
|
||||
await click(target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']"));
|
||||
|
||||
await clickSave(target);
|
||||
assert.notOk(
|
||||
target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']").checked
|
||||
);
|
||||
assert.verifySteps(["name_search", "write"]);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField in a one2many", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.models.partner_type.records.push({ id: 15, display_name: "bronze" });
|
||||
serverData.models.partner.records[0].timmy = [14, 15];
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="p">
|
||||
<tree><field name="id"/></tree>
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>
|
||||
</field>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write") {
|
||||
assert.deepEqual(args.args[1], {
|
||||
p: [[1, 1, { timmy: [[6, false, [15, 12]]] }]],
|
||||
});
|
||||
}
|
||||
},
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_data_cell"));
|
||||
|
||||
// edit the timmy field by (un)checking boxes on the widget
|
||||
const firstCheckbox = target.querySelector(".modal .form-check-input");
|
||||
await click(firstCheckbox);
|
||||
assert.ok(firstCheckbox.checked, "the checkbox should be ticked");
|
||||
const secondCheckbox = target.querySelectorAll(".modal .form-check-input")[1];
|
||||
await click(secondCheckbox);
|
||||
assert.notOk(secondCheckbox.checked, "the checkbox should be unticked");
|
||||
|
||||
await click(target.querySelector(".modal .o_form_button_save"));
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyCheckBoxesField with default values", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
serverData.models.partner.fields.timmy.default = [3];
|
||||
serverData.models.partner.fields.timmy.type = "many2many";
|
||||
serverData.models.partner_type.records.push({ id: 3, display_name: "bronze" });
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_checkboxes"/>
|
||||
</form>`,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
assert.deepEqual(
|
||||
args.args[0].timmy,
|
||||
[[6, false, [12]]],
|
||||
"correct values should have been sent to create"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[0].checked,
|
||||
"first checkbox should not be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
assert.ok(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[2].checked,
|
||||
"third checkbox should be checked"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_form_view .form-check input:checked"));
|
||||
await click(target.querySelector(".o_form_view .form-check input"));
|
||||
await click(target.querySelector(".o_form_view .form-check input"));
|
||||
await click(target.querySelector(".o_form_view .form-check input"));
|
||||
|
||||
assert.ok(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[0].checked,
|
||||
"first checkbox should be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[1].checked,
|
||||
"second checkbox should not be checked"
|
||||
);
|
||||
assert.notOk(
|
||||
target.querySelectorAll(".o_form_view .form-check input")[2].checked,
|
||||
"third checkbox should not be checked"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,430 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { press, queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char({ string: "Displayed name" });
|
||||
_records = [
|
||||
{ id: 1, name: "first record" },
|
||||
{ id: 2, name: "second record" },
|
||||
{ id: 4, name: "aaa" },
|
||||
];
|
||||
}
|
||||
|
||||
class Turtle extends models.Model {
|
||||
name = fields.Char({ string: "Displayed name" });
|
||||
partner_ids = fields.Many2many({ string: "Partner", relation: "partner" });
|
||||
_records = [
|
||||
{ id: 1, name: "leonardo", partner_ids: [] },
|
||||
{ id: 2, name: "donatello", partner_ids: [2, 4] },
|
||||
{ id: 3, name: "raphael" },
|
||||
];
|
||||
}
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
defineModels([Partner, Turtle]);
|
||||
|
||||
test("widget many2many_tags_avatar", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(queryAllTexts("[name='partner_ids'] .o_tag")).toEqual([]);
|
||||
expect("[name='partner_ids'] .o_input_dropdown input").toHaveValue("");
|
||||
|
||||
await contains("[name='partner_ids'] .o_input_dropdown input").fill("first record");
|
||||
await runAllTimers();
|
||||
expect(queryAllTexts("[name='partner_ids'] .o_tag")).toEqual(["first record"]);
|
||||
expect("[name='partner_ids'] .o_input_dropdown input").toHaveValue("");
|
||||
|
||||
await contains("[name='partner_ids'] .o_input_dropdown input").fill("abc");
|
||||
await runAllTimers();
|
||||
expect(queryAllTexts("[name='partner_ids'] .o_tag")).toEqual(["first record", "abc"]);
|
||||
expect("[name='partner_ids'] .o_input_dropdown input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar img src", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_avatar img").toHaveCount(2);
|
||||
expect(
|
||||
`.o_field_many2many_tags_avatar.o_field_widget .o_avatar:nth-child(1) img[data-src='${getOrigin()}/web/image/partner/2/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar in list view", async () => {
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
Partner._records.push({
|
||||
id,
|
||||
name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
Turtle._records.push({
|
||||
id: 4,
|
||||
name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
Turtle._records[0].partner_ids = [1];
|
||||
Turtle._records[1].partner_ids = [1, 2, 4, 5, 6, 7];
|
||||
Turtle._records[2].partner_ids = [1, 2, 4, 5, 7];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
expect(
|
||||
`.o_data_row:nth-child(1) .o_field_many2many_tags_avatar .o_avatar img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/1/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row .o_many2many_tags_avatar_cell .o_field_many2many_tags_avatar:eq(0)"
|
||||
).toHaveText("first record");
|
||||
expect(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img"
|
||||
).toHaveCount(4);
|
||||
expect(
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img"
|
||||
).toHaveCount(5);
|
||||
expect(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("+2");
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(1) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/1/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(2) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/2/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(3) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/4/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(4) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/5/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(0);
|
||||
expect(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img"
|
||||
).toHaveCount(4);
|
||||
expect(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("+9");
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const tag = queryOne(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
);
|
||||
expect(tag).toHaveAttribute("data-tooltip-template", "web.TagsList.Tooltip");
|
||||
const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]);
|
||||
expect(tooltipInfo.tags.map((tag) => tag.text).join(" ")).toBe("record 6 record 7", {
|
||||
message: "shows a tooltip on hover",
|
||||
});
|
||||
|
||||
await contains(".o_data_row .o_many2many_tags_avatar_cell:eq(0)").click();
|
||||
await contains(
|
||||
".o_data_row .o_many2many_tags_avatar_cell:eq(0) .o-autocomplete--input"
|
||||
).click();
|
||||
await contains(".o-autocomplete--dropdown-item:eq(1)").click();
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
|
||||
expect(".o_data_row:eq(0) .o_field_many2many_tags_avatar .o_avatar img").toHaveCount(2);
|
||||
|
||||
// Edit first row
|
||||
await contains(".o_data_row:nth-child(1) .o_data_cell").click();
|
||||
|
||||
// Only the first row should have tags with delete buttons.
|
||||
expect(".o_data_row:nth-child(1) .o_field_tags span .o_delete").toHaveCount(2);
|
||||
expect(".o_data_row:nth-child(2) .o_field_tags span .o_delete").toHaveCount(0);
|
||||
expect(".o_data_row:nth-child(3) .o_field_tags span .o_delete").toHaveCount(0);
|
||||
expect(".o_data_row:nth-child(4) .o_field_tags span .o_delete").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar list view - don't crash on keyboard navigation", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
arch: /*xml*/ `
|
||||
<list editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</list>
|
||||
`,
|
||||
});
|
||||
|
||||
// Edit second row
|
||||
await contains(".o_data_row:nth-child(2) .o_data_cell").click();
|
||||
|
||||
// Pressing left arrow should focus on the right-most (second) tag.
|
||||
await press("arrowleft");
|
||||
expect(".o_data_row:nth-child(2) .o_field_tags span:nth-child(2):first").toBeFocused();
|
||||
|
||||
// Pressing left arrow again should not crash and should focus on the first tag.
|
||||
await press("arrowleft");
|
||||
expect(".o_data_row:nth-child(2) .o_field_tags span:nth-child(1):first").toBeFocused();
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar in kanban view", async () => {
|
||||
expect.assertions(21);
|
||||
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
Partner._records.push({
|
||||
id,
|
||||
name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
Turtle._records.push({
|
||||
id: 4,
|
||||
name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
Turtle._records[0].partner_ids = [1];
|
||||
Turtle._records[1].partner_ids = [1, 2, 4];
|
||||
Turtle._records[2].partner_ids = [1, 2, 4, 5];
|
||||
Turtle._views = {
|
||||
form: '<form><field name="name"/></form>',
|
||||
};
|
||||
Partner._views = {
|
||||
list: '<list><field name="name"/></list>',
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
selectRecord(recordId) {
|
||||
expect(recordId).toBe(1, {
|
||||
message: "should call its selectRecord prop with the clicked record",
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_quick_assign").toHaveCount(1);
|
||||
|
||||
expect(
|
||||
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_avatar img"
|
||||
).toHaveCount(3);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar img"
|
||||
).toHaveCount(2);
|
||||
expect(
|
||||
`.o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:nth-child(1 of .o_tag) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/5/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
`.o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:nth-child(2 of .o_tag) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/4/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("+2");
|
||||
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_avatar img"
|
||||
).toHaveCount(2);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
).toHaveText("9+");
|
||||
expect(".o_field_many2many_tags_avatar .o_field_many2many_selection").toHaveCount(0);
|
||||
await contains(".o_kanban_record:nth-child(3) .o_quick_assign", { visible: false }).click();
|
||||
await animationFrame();
|
||||
expect(".o-overlay-container input").toBeFocused();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(4);
|
||||
// delete inside the popover
|
||||
await contains(".o-overlay-container .o_tag .o_delete:eq(0)", {
|
||||
visible: false,
|
||||
displayed: true,
|
||||
}).click();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(3);
|
||||
expect(".o_kanban_record:nth-child(3) .o_tag").toHaveCount(3);
|
||||
// select first non selected input
|
||||
await contains(".o-overlay-container .o-autocomplete--dropdown-item:eq(4)").click();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(4);
|
||||
expect(".o_kanban_record:nth-child(3) .o_tag").toHaveCount(2);
|
||||
// load more
|
||||
await contains(".o-overlay-container .o_m2o_dropdown_option_search_more").click();
|
||||
// first non already selected item
|
||||
await contains(".o_dialog .o_list_table .o_data_row .o_data_cell:eq(3)").click();
|
||||
expect(".o-overlay-container .o_tag").toHaveCount(5);
|
||||
expect(".o_kanban_record:nth-child(3) .o_tag").toHaveCount(2);
|
||||
expect(
|
||||
`.o_kanban_record:nth-child(2) img.o_m2m_avatar[data-src='${getOrigin()}/web/image/partner/4/avatar_128']`
|
||||
).toHaveCount(1);
|
||||
await contains(".o_kanban_record .o_field_many2many_tags_avatar img.o_m2m_avatar").click();
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar add/remove tags in kanban view", async () => {
|
||||
onRpc("web_save", ({ args }) => {
|
||||
const command = args[1].partner_ids[0];
|
||||
expect.step(`web_save: ${command[0]}-${command[1]}`);
|
||||
});
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
await contains(".o_kanban_record:eq(0) .o_quick_assign", { visible: false }).click();
|
||||
// add and directly remove an item
|
||||
await contains(".o_popover .o-autocomplete--dropdown-item:eq(0)").click();
|
||||
await contains(".o_popover .o_tag .o_delete", { visible: false }).click();
|
||||
expect.verifySteps(["web_save: 4-1", "web_save: 3-1"]);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar delete tag", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
resId: 2,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_tag").toHaveCount(2);
|
||||
|
||||
await contains(".o_field_many2many_tags_avatar.o_field_widget .o_tag .o_delete", {
|
||||
visible: false,
|
||||
}).click();
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_tag").toHaveCount(1);
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_many2many_tags_avatar.o_field_widget .o_tag").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar quick add tags and close in kanban view with keyboard navigation", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
await contains(".o_kanban_record:eq(0) .o_quick_assign", { visible: false }).click();
|
||||
// add and directly close the dropdown
|
||||
await press("Tab");
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_tag").toHaveCount(1);
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("widget many2many_tags_avatar in kanban view missing access rights", async () => {
|
||||
expect.assertions(1);
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
arch: `
|
||||
<kanban edit="0" create="0">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="name"/>
|
||||
<footer>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
expect(".o_kanban_record:eq(0) .o_field_many2many_tags_avatar .o_quick_assign").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Many2ManyTagsAvatarField: make sure that the arch context is passed to the form view call", async () => {
|
||||
Partner._views = {
|
||||
form: `<form><field name="name"/></form>`,
|
||||
};
|
||||
onRpc("onchange", (args) => {
|
||||
if (args.model === "partner" && args.kwargs.context.append_coucou === "test_value") {
|
||||
expect.step("onchange with context given");
|
||||
}
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
arch: `<list editable="top">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar" context="{ 'append_coucou': 'test_value' }"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains("div[name=partner_ids]").click();
|
||||
await contains(`div[name="partner_ids"] input`).edit("A new partner", { confirm: false });
|
||||
await runAllTimers();
|
||||
await contains(".o_m2o_dropdown_option_create_edit").click();
|
||||
|
||||
expect(".modal .o_form_view").toHaveCount(1);
|
||||
expect.verifySteps(["onchange with context given"]);
|
||||
});
|
||||
|
|
@ -1,456 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, clickSave, getFixture, selectDropdownItem } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { triggerHotkey } from "../../helpers/utils";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "first record" },
|
||||
{ id: 2, display_name: "second record" },
|
||||
{ id: 4, display_name: "aaa" },
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
turtle: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
partner_ids: { string: "Partner", type: "many2many", relation: "partner" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "leonardo", partner_ids: [] },
|
||||
{ id: 2, display_name: "donatello", partner_ids: [2, 4] },
|
||||
{ id: 3, display_name: "raphael" },
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2ManyTagsAvatarField");
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge img").dataset
|
||||
.src,
|
||||
"/web/image/partner/2/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar in list view", async function (assert) {
|
||||
const records = [];
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner.records = serverData.models.partner.records.concat(records);
|
||||
|
||||
serverData.models.turtle.records.push({
|
||||
id: 4,
|
||||
display_name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
serverData.models.turtle.records[0].partner_ids = [1];
|
||||
serverData.models.turtle.records[1].partner_ids = [1, 2, 4, 5, 6, 7];
|
||||
serverData.models.turtle.records[2].partner_ids = [1, 2, 4, 5, 7];
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row .o_field_many2many_tags_avatar img.o_m2m_avatar")
|
||||
.dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_data_row .o_many2many_tags_avatar_cell .o_field_many2many_tags_avatar"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"first record",
|
||||
"should display like many2one avatar if there is only one record"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
|
||||
4,
|
||||
"should have 4 records"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
|
||||
5,
|
||||
"should have 5 records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"+2",
|
||||
"should have +2 in o_m2m_avatar_empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(2) img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/2/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(3) img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/4/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(4) img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/5/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
|
||||
4,
|
||||
"should have 4 records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"+9",
|
||||
"should have +9 in o_m2m_avatar_empty"
|
||||
);
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const tag = target.querySelector(
|
||||
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
tag.dataset["tooltipTemplate"],
|
||||
"web.TagsList.Tooltip",
|
||||
"uses the proper tooltip template"
|
||||
);
|
||||
const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]);
|
||||
assert.strictEqual(
|
||||
tooltipInfo.tags.map((tag) => tag.text).join(" "),
|
||||
"record 6 record 7",
|
||||
"shows a tooltip on hover"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_data_row .o_many2many_tags_avatar_cell"));
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row.o_selected_row .o_many2many_tags_avatar_cell .badge",
|
||||
1,
|
||||
"should have 1 many2many badges in edit mode"
|
||||
);
|
||||
|
||||
await selectDropdownItem(target, "partner_ids", "second record");
|
||||
await click(target.querySelector(".o_list_button_save"));
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_data_row:first-child .o_field_many2many_tags_avatar .o_tag",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
|
||||
// Select the first row and enter edit mode on the x2many field.
|
||||
await click(target, ".o_data_row:nth-child(1) .o_list_record_selector input");
|
||||
await click(target, ".o_data_row:nth-child(1) .o_data_cell");
|
||||
|
||||
// Only the first row should have tags with delete buttons.
|
||||
assert.containsN(target, ".o_data_row:nth-child(1) .o_field_tags span .o_delete", 2);
|
||||
assert.containsNone(target, ".o_data_row:nth-child(2) .o_field_tags span .o_delete");
|
||||
assert.containsNone(target, ".o_data_row:nth-child(3) .o_field_tags span .o_delete");
|
||||
assert.containsNone(target, ".o_data_row:nth-child(4) .o_field_tags span .o_delete");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"widget many2many_tags_avatar list view - don't crash on keyboard navigation",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: /*xml*/ `
|
||||
<tree editable="bottom">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
// Select the 2nd row and enter edit mode on the x2many field.
|
||||
await click(target, ".o_data_row:nth-child(2) .o_list_record_selector input");
|
||||
await click(target, ".o_data_row:nth-child(2) .o_data_cell");
|
||||
|
||||
// Pressing left arrow should focus on the right-most (second) tag.
|
||||
await triggerHotkey("arrowleft");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_field_tags span:nth-child(2)"),
|
||||
document.activeElement
|
||||
);
|
||||
|
||||
// Pressing left arrow again should not crash and should focus on the first tag.
|
||||
await triggerHotkey("arrowleft");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row:nth-child(2) .o_field_tags span:nth-child(1)"),
|
||||
document.activeElement
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
const records = [];
|
||||
for (let id = 5; id <= 15; id++) {
|
||||
records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
serverData.models.partner.records = serverData.models.partner.records.concat(records);
|
||||
|
||||
serverData.models.turtle.records.push({
|
||||
id: 4,
|
||||
display_name: "crime master gogo",
|
||||
partner_ids: [1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
|
||||
});
|
||||
serverData.models.turtle.records[0].partner_ids = [1];
|
||||
serverData.models.turtle.records[1].partner_ids = [1, 2, 4];
|
||||
serverData.models.turtle.records[2].partner_ids = [1, 2, 4, 5];
|
||||
serverData.views = {
|
||||
"turtle,false,form": '<form><field name="display_name"/></form>',
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "turtle",
|
||||
serverData,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="display_name"/>
|
||||
<div class="oe_kanban_footer">
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
selectRecord(recordId) {
|
||||
assert.strictEqual(
|
||||
recordId,
|
||||
1,
|
||||
"should call its selectRecord prop with the clicked record"
|
||||
);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:first-child .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag",
|
||||
3,
|
||||
"should have 3 records"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_tag",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
).dataset.src,
|
||||
"/web/image/partner/1/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar img.o_m2m_avatar"
|
||||
)[1].dataset.src,
|
||||
"/web/image/partner/2/avatar_128",
|
||||
"should have correct avatar image"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"+2",
|
||||
"should have +2 in o_m2m_avatar_empty"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_tag",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty",
|
||||
"should have o_m2m_avatar_empty span"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
)
|
||||
.textContent.trim(),
|
||||
"9+",
|
||||
"should have 9+ in o_m2m_avatar_empty"
|
||||
);
|
||||
|
||||
// check data-tooltip attribute (used by the tooltip service)
|
||||
const tag = target.querySelector(
|
||||
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_m2m_avatar_empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
tag.dataset["tooltipTemplate"],
|
||||
"web.TagsList.Tooltip",
|
||||
"uses the proper tooltip template"
|
||||
);
|
||||
const tooltipInfo = JSON.parse(tag.dataset["tooltipInfo"]);
|
||||
assert.strictEqual(
|
||||
tooltipInfo.tags.map((tag) => tag.text).join(" "),
|
||||
"aaa record 5",
|
||||
"shows a tooltip on hover"
|
||||
);
|
||||
|
||||
await click(
|
||||
target.querySelector(".o_kanban_record .o_field_many2many_tags_avatar img.o_m2m_avatar")
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("widget many2many_tags_avatar delete tag", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "turtle",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="partner_ids" widget="many2many_tags_avatar"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
2,
|
||||
"should have 2 records"
|
||||
);
|
||||
|
||||
await click(
|
||||
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge .o_delete")
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
"should have 1 record"
|
||||
);
|
||||
|
||||
await clickSave(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_many2many_tags_avatar.o_field_widget .badge",
|
||||
"should have 1 record"
|
||||
);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,433 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickFieldDropdown,
|
||||
clickFieldDropdownItem,
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
patchWithCleanup,
|
||||
stepAllNetworkCalls,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer();
|
||||
user_id = fields.Many2one({ string: "Users", relation: "res.users" });
|
||||
_records = [
|
||||
{ id: 1, user_id: 1 },
|
||||
{ id: 2, user_id: 2 },
|
||||
{ id: 3, user_id: 1 },
|
||||
{ id: 4, user_id: false },
|
||||
];
|
||||
}
|
||||
|
||||
class Users extends models.Model {
|
||||
_name = "res.users";
|
||||
name = fields.Char();
|
||||
partner_ids = fields.One2many({ relation: "partner", relation_field: "user_id" });
|
||||
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Aline",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Christine",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Users]);
|
||||
|
||||
test.tags("desktop");
|
||||
test("basic form view flow", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=user_id] input").toHaveValue("Aline");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/1/avatar_128"]').toHaveCount(1);
|
||||
expect(".o_field_many2one_avatar > div").toHaveCount(1);
|
||||
|
||||
expect(".o_input_dropdown").toHaveCount(1);
|
||||
expect(".o_input_dropdown input").toHaveValue("Aline");
|
||||
expect(".o_external_button").toHaveCount(1);
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/1/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await clickFieldDropdown("user_id");
|
||||
expect(".o_field_many2one_selection .o_avatar_many2x_autocomplete").toHaveCount(2);
|
||||
await clickFieldDropdownItem("user_id", "Christine");
|
||||
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/2/avatar_128"]').toHaveCount(1);
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget[name=user_id] input").toHaveValue("Christine");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/2/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await contains('.o_field_widget[name="user_id"] input').clear({ confirm: "blur" });
|
||||
|
||||
expect(".o_m2o_avatar > img").toHaveCount(0);
|
||||
expect(".o_m2o_avatar > .o_m2o_avatar_empty").toHaveCount(1);
|
||||
await clickSave();
|
||||
|
||||
expect(".o_m2o_avatar > img").toHaveCount(0);
|
||||
expect(".o_m2o_avatar > .o_m2o_avatar_empty").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("onchange in form view flow", async () => {
|
||||
Partner._fields.int_field = fields.Integer({
|
||||
onChange: (obj) => {
|
||||
if (obj.int_field === 1) {
|
||||
obj.user_id = [2, "Christine"];
|
||||
} else if (obj.int_field === 2) {
|
||||
obj.user_id = false;
|
||||
} else {
|
||||
obj.user_id = [1, "Aline"]; // default value
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="user_id" widget="many2one_avatar" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=user_id]").toHaveText("Aline");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/1/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await contains("div[name=int_field] input").edit(1);
|
||||
|
||||
expect(".o_field_widget[name=user_id]").toHaveText("Christine");
|
||||
expect('.o_m2o_avatar > img[data-src="/web/image/res.users/2/avatar_128"]').toHaveCount(1);
|
||||
|
||||
await contains("div[name=int_field] input").edit(2);
|
||||
|
||||
expect(".o_field_widget[name=user_id]").toHaveText("");
|
||||
expect(".o_m2o_avatar > img").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("basic list view flow", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list><field name="user_id" widget="many2one_avatar"/></list>',
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell[name='user_id']")).toEqual([
|
||||
"Aline",
|
||||
"Christine",
|
||||
"Aline",
|
||||
"",
|
||||
]);
|
||||
const imgs = queryAll(".o_m2o_avatar > img");
|
||||
expect(imgs[0]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(imgs[1]).toHaveAttribute("data-src", "/web/image/res.users/2/avatar_128");
|
||||
expect(imgs[2]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
});
|
||||
|
||||
test("basic flow in editable list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list editable="top"><field name="user_id" widget="many2one_avatar"/></list>',
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell[name='user_id']")).toEqual([
|
||||
"Aline",
|
||||
"Christine",
|
||||
"Aline",
|
||||
"",
|
||||
]);
|
||||
|
||||
const imgs = queryAll(".o_m2o_avatar > img");
|
||||
expect(imgs[0]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(imgs[1]).toHaveAttribute("data-src", "/web/image/res.users/2/avatar_128");
|
||||
expect(imgs[2]).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
|
||||
await contains(".o_data_row .o_data_cell:eq(0)").click();
|
||||
|
||||
expect(".o_m2o_avatar > img:eq(0)").toHaveAttribute(
|
||||
"data-src",
|
||||
"/web/image/res.users/1/avatar_128"
|
||||
);
|
||||
});
|
||||
|
||||
test("Many2OneAvatar with placeholder", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name='user_id'] input").toHaveAttribute("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("click on many2one_avatar in a list view (multi_edit='1')", async () => {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
expect.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row:eq(0) .o_list_record_selector input").click();
|
||||
await contains(".o_data_row .o_data_cell [name='user_id']").click();
|
||||
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("click on many2one_avatar in an editable list view", async () => {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
expect.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row .o_data_cell [name='user_id']").click();
|
||||
expect(".o_selected_row").toHaveCount(0);
|
||||
|
||||
expect.verifySteps(["openRecord"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("click on many2one_avatar in an editable list view (editable top)", async () => {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
expect.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_row .o_data_cell [name='user_id']").click();
|
||||
expect(".o_data_row:eq(0)").toHaveClass("o_selected_row");
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("readonly many2one_avatar in form view should contain a link", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="user_id" widget="many2one_avatar" readonly="1"/></form>`,
|
||||
});
|
||||
|
||||
expect("[name='user_id'] a").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("readonly many2one_avatar in list view should not contain a link", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `<list><field name="user_id" widget="many2one_avatar"/></list>`,
|
||||
});
|
||||
|
||||
expect("[name='user_id'] a").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("readonly many2one_avatar in form view with no_open set to true", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `<form><field name="user_id" widget="many2one_avatar" readonly="1" options="{'no_open': 1}"/></form>`,
|
||||
});
|
||||
|
||||
expect("[name='user_id'] a").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("readonly many2one_avatar in list view with no_open set to false", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `<list><field name="user_id" widget="many2one_avatar" options="{'no_open': 0}"/></list>`,
|
||||
});
|
||||
|
||||
expect("[name='user_id'] a").toHaveCount(3);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("cancelling create dialog should clear value in the field", async () => {
|
||||
Users._views = {
|
||||
form: `
|
||||
<form>
|
||||
<field name="name" />
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_data_cell:eq(0)").click();
|
||||
await contains(".o_field_widget[name=user_id] input").edit("yy", { confirm: false });
|
||||
await runAllTimers();
|
||||
await clickFieldDropdownItem("user_id", "Create and edit...");
|
||||
|
||||
await contains(".o_form_button_cancel").click();
|
||||
expect(".o_field_widget[name=user_id] input").toHaveValue("");
|
||||
expect(".o_field_widget[name=user_id] span.o_m2o_avatar_empty").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("widget many2one_avatar in kanban view (load more dialog)", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
for (let id = 3; id <= 12; id++) {
|
||||
Users._records.push({
|
||||
id,
|
||||
display_name: `record ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
Users._views = {
|
||||
list: '<list><field name="display_name"/></list>',
|
||||
};
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<footer>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
|
||||
// open popover
|
||||
await contains(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > a.o_quick_assign"
|
||||
).click();
|
||||
|
||||
// load more
|
||||
await contains(".o-overlay-container .o_m2o_dropdown_option_search_more").click();
|
||||
await contains(".o_dialog .o_list_table .o_data_row .o_data_cell").click();
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
});
|
||||
|
||||
test("widget many2one_avatar in kanban view", async () => {
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<footer>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
stepAllNetworkCalls();
|
||||
|
||||
expect(
|
||||
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).toHaveCount(1);
|
||||
// open popover
|
||||
await contains(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).click();
|
||||
expect(".o-overlay-container input").toBeFocused();
|
||||
expect.verifySteps(["web_name_search"]);
|
||||
// select first input
|
||||
await contains(".o-overlay-container .o-autocomplete--dropdown-item").click();
|
||||
expect.verifySteps(["web_save"]);
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("widget many2one_avatar in kanban view without access rights", async () => {
|
||||
expect.assertions(2);
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<kanban edit="0" create="0">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<footer>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
expect(
|
||||
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
|
||||
).toHaveAttribute("data-src", "/web/image/res.users/1/avatar_128");
|
||||
expect(
|
||||
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
|
@ -1,342 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
patchWithCleanup,
|
||||
selectDropdownItem,
|
||||
triggerEvent,
|
||||
clickDiscard,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: { string: "int_field", type: "integer" },
|
||||
user_id: { string: "User", type: "many2one", relation: "user" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, user_id: 17 },
|
||||
{ id: 2, user_id: 19 },
|
||||
{ id: 3, user_id: 17 },
|
||||
{ id: 4, user_id: false },
|
||||
],
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
partner_ids: {
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "user_id",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 17,
|
||||
name: "Aline",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Christine",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("Many2OneAvatar");
|
||||
|
||||
QUnit.test("basic form view flow", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id] input").value,
|
||||
"Aline"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/17/avatar_128"]'
|
||||
);
|
||||
assert.containsOnce(target, '.o_field_many2one_avatar > div[data-tooltip="Aline"]');
|
||||
|
||||
assert.containsOnce(target, ".o_input_dropdown");
|
||||
assert.strictEqual(target.querySelector(".o_input_dropdown input").value, "Aline");
|
||||
assert.containsOnce(target, ".o_external_button");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/17/avatar_128"]'
|
||||
);
|
||||
|
||||
await selectDropdownItem(target, "user_id", "Christine");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/19/avatar_128"]'
|
||||
);
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id] input").value,
|
||||
"Christine"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/19/avatar_128"]'
|
||||
);
|
||||
|
||||
await editInput(target, '.o_field_widget[name="user_id"] input', "");
|
||||
|
||||
assert.containsNone(target, ".o_m2o_avatar > img");
|
||||
assert.containsOnce(target, ".o_m2o_avatar > .o_m2o_avatar_empty");
|
||||
await clickSave(target);
|
||||
|
||||
assert.containsNone(target, ".o_m2o_avatar > img");
|
||||
assert.containsOnce(target, ".o_m2o_avatar > .o_m2o_avatar_empty");
|
||||
});
|
||||
|
||||
QUnit.test("onchange in form view flow", async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
int_field: function (obj) {
|
||||
if (obj.int_field === 1) {
|
||||
obj.user_id = [19, "Christine"];
|
||||
} else if (obj.int_field === 2) {
|
||||
obj.user_id = false;
|
||||
} else {
|
||||
obj.user_id = [17, "Aline"]; // default value
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="int_field"/>
|
||||
<field name="user_id" widget="many2one_avatar" readonly="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id]").textContent.trim(),
|
||||
"Aline"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/17/avatar_128"]'
|
||||
);
|
||||
|
||||
await editInput(target, "div[name=int_field] input", 1);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id]").textContent.trim(),
|
||||
"Christine"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'.o_m2o_avatar > img[data-src="/web/image/user/19/avatar_128"]'
|
||||
);
|
||||
|
||||
await editInput(target, "div[name=int_field] input", 2);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=user_id]").textContent.trim(),
|
||||
""
|
||||
);
|
||||
assert.containsNone(target, ".o_m2o_avatar > img");
|
||||
});
|
||||
|
||||
QUnit.test("basic list view flow", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<tree><field name="user_id" widget="many2one_avatar"/></tree>',
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
|
||||
["Aline", "Christine", "Aline", ""]
|
||||
);
|
||||
const imgs = target.querySelectorAll(".o_m2o_avatar > img");
|
||||
assert.strictEqual(imgs[0].dataset.src, "/web/image/user/17/avatar_128");
|
||||
assert.strictEqual(imgs[1].dataset.src, "/web/image/user/19/avatar_128");
|
||||
assert.strictEqual(imgs[2].dataset.src, "/web/image/user/17/avatar_128");
|
||||
});
|
||||
|
||||
QUnit.test("basic flow in editable list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
arch: '<tree editable="top"><field name="user_id" widget="many2one_avatar"/></tree>',
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
|
||||
["Aline", "Christine", "Aline", ""]
|
||||
);
|
||||
|
||||
const imgs = target.querySelectorAll(".o_m2o_avatar > img");
|
||||
assert.strictEqual(imgs[0].dataset.src, "/web/image/user/17/avatar_128");
|
||||
assert.strictEqual(imgs[1].dataset.src, "/web/image/user/19/avatar_128");
|
||||
assert.strictEqual(imgs[2].dataset.src, "/web/image/user/17/avatar_128");
|
||||
|
||||
await click(target.querySelectorAll(".o_data_row .o_data_cell")[0]);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_m2o_avatar > img").dataset.src,
|
||||
"/web/image/user/17/avatar_128"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Many2OneAvatar with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch:
|
||||
'<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='user_id'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("click on many2one_avatar in a list view (multi_edit='1')", async function (assert) {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
assert.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree multi_edit="1">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll(".o_data_row")[0], ".o_list_record_selector input");
|
||||
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
|
||||
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("click on many2one_avatar in an editable list view", async function (assert) {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
assert.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll(".o_data_row")[0], ".o_list_record_selector input");
|
||||
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
|
||||
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("click on many2one_avatar in an editable list view", async function (assert) {
|
||||
const listView = registry.category("views").get("list");
|
||||
patchWithCleanup(listView.Controller.prototype, {
|
||||
openRecord() {
|
||||
assert.step("openRecord");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
|
||||
assert.containsNone(target, ".o_selected_row");
|
||||
|
||||
assert.verifySteps(["openRecord"]);
|
||||
});
|
||||
|
||||
QUnit.test("cancelling create dialog should clear value in the field", async function (assert) {
|
||||
serverData.views = {
|
||||
"user,false,form": `
|
||||
<form>
|
||||
<field name="name" />
|
||||
</form>`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="user_id" widget="many2one_avatar"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll(".o_data_cell")[0]);
|
||||
const input = target.querySelector(".o_field_widget[name=user_id] input");
|
||||
input.value = "yy";
|
||||
await triggerEvent(input, null, "input");
|
||||
await click(target, ".o_field_widget[name=user_id] input");
|
||||
await selectDropdownItem(target, "user_id", "Create and edit...");
|
||||
|
||||
await clickDiscard(target.querySelector(".modal"));
|
||||
assert.strictEqual(target.querySelector(".o_field_widget[name=user_id] input").value, "");
|
||||
assert.containsOnce(target, ".o_field_widget[name=user_id] span.o_m2o_avatar_empty");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { mockUserAgent, mockVibrate, runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
getKwArgs,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import * as BarcodeScanner from "@web/core/barcode/barcode_dialog";
|
||||
|
||||
class Product extends models.Model {
|
||||
_name = "product.product";
|
||||
name = fields.Char();
|
||||
barcode = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 111,
|
||||
name: "product_cable_management_box",
|
||||
barcode: "601647855631",
|
||||
},
|
||||
{
|
||||
id: 112,
|
||||
name: "product_n95_mask",
|
||||
barcode: "601647855632",
|
||||
},
|
||||
{
|
||||
id: 113,
|
||||
name: "product_surgical_mask",
|
||||
barcode: "601647855633",
|
||||
},
|
||||
];
|
||||
// to allow the search in barcode too
|
||||
name_search() {
|
||||
const result = super.name_search(...arguments);
|
||||
const kwargs = getKwArgs(arguments, "name", "domain", "operator", "limit");
|
||||
for (const record of this) {
|
||||
if (record.barcode === kwargs.name) {
|
||||
result.push([record.id, record.name]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
_views = {
|
||||
kanban: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="barcode"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
search: "<search/>",
|
||||
};
|
||||
}
|
||||
|
||||
class SaleOrderLine extends models.Model {
|
||||
id = fields.Integer();
|
||||
product_id = fields.Many2one({
|
||||
relation: "product.product",
|
||||
});
|
||||
}
|
||||
|
||||
class User extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
defineModels([Product, SaleOrderLine, User]);
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserAgent("android");
|
||||
mockVibrate((pattern) => expect.step(`vibrate:${pattern}`));
|
||||
});
|
||||
|
||||
test("Many2OneBarcode component should display the barcode icon", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id" widget="many2one_barcode"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
expect(".o_barcode").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("barcode button with single results", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = Product._records[0];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => selectedRecordTest.barcode,
|
||||
});
|
||||
|
||||
onRpc("sale.order.line", "web_save", (args) => {
|
||||
const selectedId = args.args[1]["product_id"];
|
||||
expect(selectedId).toBe(selectedRecordTest.id, {
|
||||
message: `product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`,
|
||||
});
|
||||
return args.parent();
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id" options="{'can_scan_barcode': True}"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(".o_barcode").toHaveCount(1);
|
||||
|
||||
await contains(".o_barcode").click();
|
||||
await clickSave();
|
||||
|
||||
expect.verifySteps(["vibrate:100"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("barcode button with multiple results on desktop", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = Product._records[1];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => "mask",
|
||||
});
|
||||
|
||||
onRpc("sale.order.line", "web_save", (args) => {
|
||||
const selectedId = args.args[1]["product_id"];
|
||||
expect(selectedId).toBe(selectedRecordTest.id, {
|
||||
message: `product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`,
|
||||
});
|
||||
return args.parent();
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id" options="{'can_scan_barcode': True}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_barcode").toHaveCount(1);
|
||||
|
||||
await contains(".o_barcode").click();
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete--dropdown-menu").toHaveCount(1);
|
||||
|
||||
expect(
|
||||
".o-autocomplete--dropdown-menu .o-autocomplete--dropdown-item.ui-menu-item:not(.o_m2o_dropdown_option)"
|
||||
).toHaveCount(2);
|
||||
|
||||
await contains(
|
||||
".o-autocomplete--dropdown-menu .o-autocomplete--dropdown-item:nth-child(1)"
|
||||
).click();
|
||||
await clickSave();
|
||||
expect.verifySteps(["vibrate:100"]);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("barcode button with multiple results on mobile", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = Product._records[1];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => "mask",
|
||||
});
|
||||
|
||||
onRpc("sale.order.line", "web_save", (args) => {
|
||||
const selectedId = args.args[1]["product_id"];
|
||||
expect(selectedId).toBe(selectedRecordTest.id, {
|
||||
message: `product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`,
|
||||
});
|
||||
return args.parent();
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `<form><field name="product_id" options="{'can_scan_barcode': True}"/></form>`,
|
||||
});
|
||||
|
||||
expect(".o_barcode").toHaveCount(1, { message: "has scanner barcode button" });
|
||||
|
||||
await contains(".o_barcode").click();
|
||||
|
||||
expect(".modal-dialog.modal-lg").toHaveCount(1, {
|
||||
message: "there should be one modal opened in full screen",
|
||||
});
|
||||
expect(".modal-dialog.modal-lg .o_kanban_record:not(.o_kanban_ghost)").toHaveCount(2, {
|
||||
message: "there should be 2 records displayed",
|
||||
});
|
||||
|
||||
await contains(".o_kanban_record:nth-child(1)").click();
|
||||
await clickSave();
|
||||
expect.verifySteps(["vibrate:100"]);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("many2one with barcode show all records", async () => {
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = Product._records[1];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => selectedRecordTest.barcode,
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order.line",
|
||||
arch: `<form><field name="product_id" options="{'can_scan_barcode': True}"/></form>`,
|
||||
});
|
||||
|
||||
// Select one product
|
||||
await contains(".o_barcode").click();
|
||||
|
||||
// Click on the input to show all records
|
||||
await contains(".o_input_dropdown > input").click();
|
||||
|
||||
expect(".modal-dialog.modal-lg").toHaveCount(1, {
|
||||
message: "there should be one modal opened in full screen",
|
||||
});
|
||||
expect(".modal-dialog.modal-lg .o_kanban_record:not(.o_kanban_ghost)").toHaveCount(3, {
|
||||
message: "there should be 3 records displayed",
|
||||
});
|
||||
expect.verifySteps(["vibrate:100"]);
|
||||
});
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { click, clickSave, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
import * as BarcodeScanner from "@web/webclient/barcode/barcode_scanner";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
const CREATE = "create";
|
||||
const NAME_SEARCH = "name_search";
|
||||
const PRODUCT_PRODUCT = "product.product";
|
||||
const SALE_ORDER_LINE = "sale_order_line";
|
||||
const PRODUCT_FIELD_NAME = "product_id";
|
||||
|
||||
// MockRPC to allow the search in barcode too
|
||||
async function barcodeMockRPC(route, args, performRPC) {
|
||||
if (args.method === NAME_SEARCH && args.model === PRODUCT_PRODUCT) {
|
||||
const result = await performRPC(route, args);
|
||||
const records = serverData.models[PRODUCT_PRODUCT].records
|
||||
.filter((record) => record.barcode === args.kwargs.name)
|
||||
.map((record) => [record.id, record.name]);
|
||||
return records.concat(result);
|
||||
}
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
[PRODUCT_PRODUCT]: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
name: {},
|
||||
barcode: {},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 111,
|
||||
name: "product_cable_management_box",
|
||||
barcode: "601647855631",
|
||||
},
|
||||
{
|
||||
id: 112,
|
||||
name: "product_n95_mask",
|
||||
barcode: "601647855632",
|
||||
},
|
||||
{
|
||||
id: 113,
|
||||
name: "product_surgical_mask",
|
||||
barcode: "601647855633",
|
||||
},
|
||||
],
|
||||
},
|
||||
[SALE_ORDER_LINE]: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
[PRODUCT_FIELD_NAME]: {
|
||||
string: PRODUCT_FIELD_NAME,
|
||||
type: "many2one",
|
||||
relation: PRODUCT_PRODUCT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
patchWithCleanup(AutoComplete, {
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
// simulate a environment with a camera/webcam
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
Object.assign({}, browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
navigator: {
|
||||
userAgent: "Chrome/0.0.0 (Linux; Android 13; Odoo TestSuite)",
|
||||
mediaDevices: {
|
||||
getUserMedia: () => [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("Many2OneField Barcode (Desktop)");
|
||||
|
||||
QUnit.test(
|
||||
"Many2OneBarcode component should display the barcode icon",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" widget="many2one_barcode"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
const scanButton = target.querySelector(".o_barcode");
|
||||
assert.containsOnce(target, scanButton, "has scanner barcode button");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("barcode button with single results", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = serverData.models[PRODUCT_PRODUCT].records[0];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => selectedRecordTest.barcode,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" options="{'can_scan_barcode': True}"/>
|
||||
</form>
|
||||
`,
|
||||
async mockRPC(route, args, performRPC) {
|
||||
if (args.method === CREATE && args.model === SALE_ORDER_LINE) {
|
||||
const selectedId = args.args[0][PRODUCT_FIELD_NAME];
|
||||
assert.equal(
|
||||
selectedId,
|
||||
selectedRecordTest.id,
|
||||
`product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`
|
||||
);
|
||||
return performRPC(route, args, performRPC);
|
||||
}
|
||||
return barcodeMockRPC(route, args, performRPC);
|
||||
},
|
||||
});
|
||||
|
||||
const scanButton = target.querySelector(".o_barcode");
|
||||
assert.containsOnce(target, scanButton, "has scanner barcode button");
|
||||
|
||||
await click(target, ".o_barcode");
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("barcode button with multiple results", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = serverData.models[PRODUCT_PRODUCT].records[1];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => "mask",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" options="{'can_scan_barcode': True}"/>
|
||||
</form>`,
|
||||
async mockRPC(route, args, performRPC) {
|
||||
if (args.method === CREATE && args.model === SALE_ORDER_LINE) {
|
||||
const selectedId = args.args[0][PRODUCT_FIELD_NAME];
|
||||
assert.equal(
|
||||
selectedId,
|
||||
selectedRecordTest.id,
|
||||
`product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`
|
||||
);
|
||||
return performRPC(route, args, performRPC);
|
||||
}
|
||||
return barcodeMockRPC(route, args, performRPC);
|
||||
},
|
||||
});
|
||||
|
||||
const scanButton = target.querySelector(".o_barcode");
|
||||
assert.containsOnce(target, scanButton, "has scanner barcode button");
|
||||
|
||||
await click(target, ".o_barcode");
|
||||
|
||||
const autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
autocompleteDropdown,
|
||||
"there should be one autocomplete dropdown opened"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
autocompleteDropdown,
|
||||
".o-autocomplete--dropdown-item.ui-menu-item:not(.o_m2o_dropdown_option)",
|
||||
2,
|
||||
"there should be 2 records displayed"
|
||||
);
|
||||
|
||||
await click(autocompleteDropdown, ".o-autocomplete--dropdown-item:nth-child(1)");
|
||||
await clickSave(target);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,232 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickFieldDropdownItem,
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
selectFieldDropdownItem,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
model = fields.Char({
|
||||
string: "Resource Model",
|
||||
});
|
||||
res_id = fields.Many2oneReference({
|
||||
string: "Resource Id",
|
||||
model_field: "model",
|
||||
relation: "partner.type",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, model: "partner.type", res_id: 10 },
|
||||
{ id: 2, res_id: false },
|
||||
];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
id = fields.Integer();
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 10, name: "gold" },
|
||||
{ id: 14, name: "silver" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("Many2OneReferenceField in form view", async () => {
|
||||
mockService("action", {
|
||||
doAction() {
|
||||
expect.step("doAction");
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("get_formview_action", ({ model, args }) => {
|
||||
expect.step(`opening ${model} ${args[0][0]}`);
|
||||
return false;
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("gold");
|
||||
expect(".o_field_widget[name=res_id] .o_external_button").toHaveCount(1);
|
||||
|
||||
await contains(".o_field_widget[name=res_id] .o_external_button", { visible: false }).click();
|
||||
expect.verifySteps(["opening partner.type 10", "doAction"]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceField in list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<list>
|
||||
<field name="model" column_invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual(["gold", ""]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceField with no_open option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id" options="{'no_open': 1}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("gold");
|
||||
expect(".o_field_widget[name=res_id] .o_external_button").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Many2OneReferenceField edition: unset", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args).toEqual([[2], { model: "partner.type", res_id: 14 }]);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=res_id] input").toHaveCount(0);
|
||||
|
||||
await contains(".o_field_widget[name=model] input").edit("partner.type");
|
||||
|
||||
expect(".o_field_widget[name=res_id] input").toHaveCount(1);
|
||||
|
||||
await selectFieldDropdownItem("res_id", "silver");
|
||||
expect(".o_field_widget[name=res_id] input").toHaveValue("silver");
|
||||
|
||||
await clickSave();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Many2OneReferenceField set value with search more", async () => {
|
||||
PartnerType._views = {
|
||||
list: `<list><field name="name"/></list>`,
|
||||
};
|
||||
PartnerType._records = [
|
||||
{ id: 1, name: "type 1" },
|
||||
{ id: 2, name: "type 2" },
|
||||
{ id: 3, name: "type 3" },
|
||||
{ id: 4, name: "type 4" },
|
||||
{ id: 5, name: "type 5" },
|
||||
{ id: 6, name: "type 6" },
|
||||
{ id: 7, name: "type 7" },
|
||||
{ id: 8, name: "type 8" },
|
||||
{ id: 9, name: "type 9" },
|
||||
];
|
||||
Partner._records[0].res_id = 1;
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("type 1");
|
||||
await selectFieldDropdownItem("res_id", "Search more...");
|
||||
expect(".o_dialog .o_list_view").toHaveCount(1);
|
||||
await contains(".o_data_row .o_data_cell:eq(6)").click();
|
||||
expect(".o_dialog .o_list_view").toHaveCount(0);
|
||||
expect(".o_field_widget input").toHaveValue("type 7");
|
||||
expect.verifySteps([
|
||||
"get_views", // form view
|
||||
"web_read", // partner id 1
|
||||
"web_name_search", // many2one
|
||||
"get_views", // Search more...
|
||||
"web_search_read", // SelectCreateDialog
|
||||
"has_group",
|
||||
"web_read", // read selected value
|
||||
]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Many2OneReferenceField: quick create a value", async () => {
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("gold");
|
||||
|
||||
await contains(".o_field_widget[name='res_id'] input").edit("new value", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(
|
||||
".o_field_widget[name='res_id'] .dropdown-menu .o_m2o_dropdown_option_create"
|
||||
).toHaveCount(1);
|
||||
await clickFieldDropdownItem("res_id", `Create "new value"`);
|
||||
expect(".o_field_widget input").toHaveValue("new value");
|
||||
expect.verifySteps(["get_views", "web_read", "web_name_search", "name_create"]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceField with no_create option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id" options="{'no_create': 1}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='res_id'] input").edit("new value", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(
|
||||
".o_field_widget[name='res_id'] .dropdown-menu .o_m2o_dropdown_option_create"
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, getNodesTextContent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
res_id: {
|
||||
string: "Ressource Id",
|
||||
type: "many2one_reference",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, res_id: 10 },
|
||||
{ id: 2, res_id: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2OneReferenceField");
|
||||
|
||||
QUnit.test("Many2OneReferenceField in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="res_id"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10");
|
||||
});
|
||||
|
||||
QUnit.test("Many2OneReferenceField in list view", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<list><field name="res_id"/></list>',
|
||||
});
|
||||
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_data_cell")), ["10", ""]);
|
||||
});
|
||||
|
||||
QUnit.test("should be 0 when unset", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: '<form><field name="res_id"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_widget input").value, "");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
model = fields.Char({
|
||||
string: "Resource Model",
|
||||
});
|
||||
res_id = fields.Many2oneReference({
|
||||
string: "Resource Id",
|
||||
model_field: "model",
|
||||
relation: "partner.type",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, model: "partner.type", res_id: 10 },
|
||||
{ id: 2, res_id: false },
|
||||
];
|
||||
}
|
||||
|
||||
class PartnerType extends models.Model {
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 10, name: "gold" },
|
||||
{ id: 14, name: "silver" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, PartnerType]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("Many2OneReferenceIntegerField in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="res_id" widget="many2one_reference_integer"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("10");
|
||||
});
|
||||
|
||||
test("Many2OneReferenceIntegerField in list view", async () => {
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<list><field name="res_id" widget="many2one_reference_integer"/></list>',
|
||||
});
|
||||
|
||||
expect(queryAllTexts(".o_data_cell")).toEqual(["10", ""]);
|
||||
});
|
||||
|
||||
test("Many2OneReferenceIntegerField: unset value in form view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
arch: '<form><field name="res_id" widget="many2one_reference_integer"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("");
|
||||
});
|
||||
|
|
@ -0,0 +1,798 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Partner extends models.Model {
|
||||
name = fields.Char();
|
||||
int_field = fields.Integer();
|
||||
float_field = fields.Float({
|
||||
digits: [16, 1],
|
||||
});
|
||||
p = fields.One2many({ relation: "partner" });
|
||||
currency_id = fields.Many2one({ relation: "res.currency" });
|
||||
monetary_field = fields.Monetary({ currency_field: "currency_id" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, int_field: 10, float_field: 0.44444 },
|
||||
{ id: 2, int_field: 0, float_field: 0, currency_id: 2 },
|
||||
{ id: 3, int_field: 80, float_field: -3.89859 },
|
||||
{ id: 4, int_field: 0, float_field: 0 },
|
||||
{ id: 5, int_field: -4, float_field: 9.1, monetary_field: 9.1, currency_id: 1 },
|
||||
{ id: 6, float_field: 3.9, monetary_field: 4.2, currency_id: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
class Currency extends models.Model {
|
||||
_name = "res.currency";
|
||||
|
||||
name = fields.Char();
|
||||
symbol = fields.Char({ string: "Currency Sumbol" });
|
||||
position = fields.Selection({
|
||||
selection: [
|
||||
["after", "A"],
|
||||
["before", "B"],
|
||||
],
|
||||
});
|
||||
inverse_rate = fields.Float();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before", inverse_rate: 1 },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after", inverse_rate: 0.5 },
|
||||
{
|
||||
id: 3,
|
||||
name: "VEF",
|
||||
symbol: "Bs.F",
|
||||
position: "after",
|
||||
inverse_rate: 0.3,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Currency]);
|
||||
|
||||
onRpc("has_group", () => true);
|
||||
|
||||
test("basic flow in form view - float field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_monetary > div.text-nowrap").toHaveCount(1);
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_monetary input").edit("108.2458938598598");
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly after the blur",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("basic flow in form view - monetary field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_monetary input").edit("108.2458938598598");
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly after the blur",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("108.25", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("rounding using formula in form view - float field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// Test computation and rounding
|
||||
await contains(".o_field_monetary input").edit("=100/3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("33.33", {
|
||||
message: "The new value should be calculated and rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("rounding using formula in form view - monetary field", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// Test computation and rounding
|
||||
await contains(".o_field_monetary input").edit("=100/3");
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("33.33", {
|
||||
message: "The new value should be calculated and rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("with currency digits != 2 - float field", async () => {
|
||||
serverState.currencies = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before" },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after" },
|
||||
{
|
||||
id: 3,
|
||||
name: "VEF",
|
||||
symbol: "Bs.F",
|
||||
position: "after",
|
||||
digits: [0, 4],
|
||||
},
|
||||
];
|
||||
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 99.1234,
|
||||
currency_id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("99.1234", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("Bs.F", {
|
||||
message: "The input should be superposed with a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("99.111111111");
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The value should should be formatted on blur.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("with currency digits != 2 - monetary field", async () => {
|
||||
serverState.currencies = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before" },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after" },
|
||||
{
|
||||
id: 3,
|
||||
name: "VEF",
|
||||
symbol: "Bs.F",
|
||||
position: "after",
|
||||
digits: [0, 4],
|
||||
},
|
||||
];
|
||||
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 99.1234,
|
||||
monetary_field: 99.1234,
|
||||
currency_id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("99.1234", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("Bs.F", {
|
||||
message: "The input should be superposed with a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("99.111111111");
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The value should should be formatted on blur.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("99.1111", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("basic flow in editable list view - float field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 9.1,
|
||||
monetary_field: 9.1,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
float_field: 15.3,
|
||||
monetary_field: 15.3,
|
||||
currency_id: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
float_field: 0,
|
||||
monetary_field: 0,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
float_field: 5.0,
|
||||
monetary_field: 5.0,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const dollarValues = queryAll("td:contains($)");
|
||||
expect(dollarValues).toHaveLength(2, { message: "Only 2 line has dollar as a currency." });
|
||||
|
||||
const euroValues = queryAll("td:contains(€)");
|
||||
expect(euroValues).toHaveLength(1, { message: "Only 1 line has euro as a currency." });
|
||||
|
||||
const noCurrencyValues = queryAll("td.o_data_cell").filter(
|
||||
(x) => !(x.textContent.includes("€") || x.textContent.includes("$"))
|
||||
);
|
||||
expect(noCurrencyValues).toHaveLength(1, { message: "Only 1 line has no currency." });
|
||||
|
||||
// switch to edit mode
|
||||
const dollarCell = queryFirst("td.o_field_cell");
|
||||
await contains(dollarCell).click();
|
||||
|
||||
expect(dollarCell.children).toHaveLength(1, {
|
||||
message: "The cell td should only contain the special div of monetary widget.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable monetary float.",
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("108.2458938598598", { confirm: "blur" });
|
||||
expect(dollarCell).toHaveText("$ 108.25", { message: "The new value should be correct" });
|
||||
});
|
||||
|
||||
test("basic flow in editable list view - monetary field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 9.1,
|
||||
monetary_field: 9.1,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
float_field: 15.3,
|
||||
monetary_field: 15.3,
|
||||
currency_id: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
float_field: 0,
|
||||
monetary_field: 0,
|
||||
currency_id: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
float_field: 5.0,
|
||||
monetary_field: 5.0,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const dollarValues = queryAll("td:contains($)");
|
||||
expect(dollarValues).toHaveLength(2, { message: "Only 2 line has dollar as a currency." });
|
||||
|
||||
const euroValues = queryAll("td:contains(€)");
|
||||
expect(euroValues).toHaveLength(1, { message: "Only 1 line has euro as a currency." });
|
||||
|
||||
const noCurrencyValues = queryAll("td.o_data_cell").filter(
|
||||
(x) => !(x.textContent.includes("€") || x.textContent.includes("$"))
|
||||
);
|
||||
expect(noCurrencyValues).toHaveLength(1, { message: "Only 1 line has no currency." });
|
||||
|
||||
// switch to edit mode
|
||||
const dollarCell = queryFirst("td.o_field_cell");
|
||||
await contains(dollarCell).click();
|
||||
|
||||
expect(dollarCell.children).toHaveLength(1, {
|
||||
message: "The cell td should only contain the special div of monetary widget.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveCount(1, {
|
||||
message: "The view should have 1 input for editable monetary float.",
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("9.10", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_widget input").edit("108.2458938598598", { confirm: "blur" });
|
||||
expect(dollarCell).toHaveText("$ 108.25", { message: "The new value should be correct" });
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("changing currency updates the field - float field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 4.2,
|
||||
monetary_field: 4.2,
|
||||
currency_id: 1,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_many2one_selection input").click();
|
||||
await contains(".o-autocomplete--dropdown-item:contains(EUR)").click();
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("€", {
|
||||
message:
|
||||
"The input should be preceded by a span containing the currency symbol added on blur.",
|
||||
});
|
||||
expect(".o_field_monetary input").toHaveValue("4.20");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_monetary input").toHaveValue("4.20", {
|
||||
message: "The new value should still be correct.",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("changing currency updates the field - monetary field", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: 4.2,
|
||||
monetary_field: 4.2,
|
||||
currency_id: 1,
|
||||
},
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_many2one_selection input").click();
|
||||
await contains(".o-autocomplete--dropdown-item:contains(EUR)").click();
|
||||
|
||||
expect(".o_field_widget .o_input span:eq(1)").toHaveText("€", {
|
||||
message:
|
||||
"The input should be preceded by a span containing the currency symbol added on blur.",
|
||||
});
|
||||
expect(".o_field_monetary input").toHaveValue("4.20");
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_monetary input").toHaveValue("4.20", {
|
||||
message: "The new value should still be correct.",
|
||||
});
|
||||
});
|
||||
|
||||
test("MonetaryField with monetary field given in options", async () => {
|
||||
Partner._fields.company_currency_id = fields.Many2one({
|
||||
string: "Company Currency",
|
||||
relation: "res.currency",
|
||||
});
|
||||
Partner._records[4].company_currency_id = 2;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<sheet>
|
||||
<field name="monetary_field" options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="company_currency_id"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 5,
|
||||
});
|
||||
|
||||
expect(".o_field_monetary").toHaveText("9.10 €", {
|
||||
message: "field monetary should be formatted with correct currency",
|
||||
});
|
||||
});
|
||||
|
||||
test("should keep the focus when being edited in x2many lists", async () => {
|
||||
Partner._fields.currency_id.default = 1;
|
||||
Partner._fields.m2m = fields.Many2many({
|
||||
relation: "partner",
|
||||
default: [[4, 2]],
|
||||
});
|
||||
Partner._views = {
|
||||
list: `
|
||||
<list editable="bottom">
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</list>`,
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="p"/>
|
||||
<field name="m2m"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// test the monetary field inside the one2many
|
||||
await contains(".o_field_x2many_list_row_add a").click();
|
||||
await contains(".o_field_widget[name=float_field] input").edit("22", { confirm: "blur" });
|
||||
|
||||
expect(".o_field_widget[name=p] .o_field_widget[name=float_field] span").toHaveInnerHTML(
|
||||
"$ 22.00",
|
||||
{ type: "html" }
|
||||
);
|
||||
|
||||
// test the monetary field inside the many2many
|
||||
await contains(".o_field_widget[name=m2m] .o_data_cell").click();
|
||||
await contains(".o_field_widget[name=float_field] input").edit("22", { confirm: "blur" });
|
||||
expect(".o_field_widget[name=m2m] .o_field_widget[name=float_field] span").toHaveInnerHTML(
|
||||
"22.00 €",
|
||||
{ type: "html" }
|
||||
);
|
||||
});
|
||||
|
||||
test("MonetaryField with currency set by an onchange", async () => {
|
||||
// this test ensures that the monetary field can be re-rendered with and
|
||||
// without currency (which can happen as the currency can be set by an
|
||||
// onchange)
|
||||
Partner._onChanges = {
|
||||
int_field: function (obj) {
|
||||
obj.currency_id = obj.int_field ? 2 : null;
|
||||
},
|
||||
};
|
||||
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="int_field"/>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
await contains(".o_control_panel_main_buttons .o_list_button_add").click();
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] input").toHaveCount(1, {
|
||||
message: "monetary field should have been rendered correctly (without currency)",
|
||||
});
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] span").toHaveCount(2, {
|
||||
message: "monetary field should have been rendered correctly (without currency)",
|
||||
});
|
||||
|
||||
// set a value for int_field -> should set the currency and re-render float_field
|
||||
await contains(".o_field_widget[name=int_field] input").edit("7", { confirm: "blur" });
|
||||
await contains(".o_field_cell[name=int_field]").click();
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] input").toHaveCount(1, {
|
||||
message: "monetary field should have been re-rendered correctly (with currency)",
|
||||
});
|
||||
expect(
|
||||
queryAllTexts(".o_selected_row .o_field_widget[name=float_field] .o_input span")
|
||||
).toEqual(["0.00", "€"], {
|
||||
message: "monetary field should have been re-rendered correctly (with currency)",
|
||||
});
|
||||
await contains(".o_field_widget[name=float_field] input").click();
|
||||
expect(".o_field_widget[name=float_field] input").toBeFocused({
|
||||
message: "focus should be on the float_field field's input",
|
||||
});
|
||||
|
||||
// unset the value of int_field -> should unset the currency and re-render float_field
|
||||
await contains(".o_field_widget[name=int_field]").click();
|
||||
await contains(".o_field_widget[name=int_field] input").edit("0", { confirm: "blur" });
|
||||
await contains(".o_field_cell[name=int_field]").click();
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] input").toHaveCount(1, {
|
||||
message: "monetary field should have been re-rendered correctly (without currency)",
|
||||
});
|
||||
expect(".o_selected_row .o_field_widget[name=float_field] span").toHaveCount(2, {
|
||||
message: "monetary field should have been re-rendered correctly (without currency)",
|
||||
});
|
||||
await contains(".o_field_widget[name=float_field] input").click();
|
||||
expect(".o_field_widget[name=float_field] input").toBeFocused({
|
||||
message: "focus should be on the float_field field's input",
|
||||
});
|
||||
});
|
||||
|
||||
test("float widget on monetary field", async () => {
|
||||
Partner._fields.monetary = fields.Monetary({ currency_field: "currency_id" });
|
||||
Partner._records[0].monetary = 9.99;
|
||||
Partner._records[0].currency_id = 1;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form edit="0">
|
||||
<sheet>
|
||||
<field name="monetary" widget="float"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=monetary]").toHaveText("9.99", {
|
||||
message: "value should be correctly formatted (with the float formatter)",
|
||||
});
|
||||
});
|
||||
|
||||
test("float field with monetary widget and decimal precision", async () => {
|
||||
Partner._records = [
|
||||
{
|
||||
id: 1,
|
||||
float_field: -8.89859,
|
||||
currency_id: 1,
|
||||
},
|
||||
];
|
||||
serverState.currencies = [
|
||||
{ id: 1, name: "USD", symbol: "$", position: "before", digits: [0, 4] },
|
||||
{ id: 2, name: "EUR", symbol: "€", position: "after" },
|
||||
];
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="float_field" widget="monetary" options="{'field_digits': True}"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("-8.9", {
|
||||
message: "The input should be rendered without the currency symbol.",
|
||||
});
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
|
||||
await contains(".o_field_monetary input").edit("109.2458938598598");
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("109.2", {
|
||||
message: "The value should should be formatted on blur.",
|
||||
});
|
||||
|
||||
await clickSave();
|
||||
expect(".o_field_widget input").toHaveValue("109.2", {
|
||||
message: "The new value should be rounded properly.",
|
||||
});
|
||||
});
|
||||
|
||||
test("MonetaryField without currency symbol", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="float_field" widget="monetary" options="{'no_symbol': True}" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// Non-breaking space between the currency and the amount
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("9.10", {
|
||||
message: "The currency symbol is not displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("required monetary field with zero value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field" required="1"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1);
|
||||
expect("[name=monetary_field] input").toHaveValue("0.00");
|
||||
});
|
||||
|
||||
test("uses 'currency_id' as currency field by default", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 6,
|
||||
});
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1);
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
});
|
||||
|
||||
test("automatically uses currency_field if defined", async () => {
|
||||
Partner._fields.custom_currency_id = fields.Many2one({
|
||||
string: "Currency",
|
||||
relation: "res.currency",
|
||||
});
|
||||
Partner._fields.monetary_field.currency_field = "custom_currency_id";
|
||||
Partner._records[5].custom_currency_id = 1;
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="monetary_field"/>
|
||||
<field name="custom_currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 6,
|
||||
});
|
||||
|
||||
expect(".o_form_editable").toHaveCount(1);
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$", {
|
||||
message: "The input should be preceded by a span containing the currency symbol.",
|
||||
});
|
||||
});
|
||||
|
||||
test("monetary field with pending onchange", async () => {
|
||||
const def = new Deferred();
|
||||
Partner._onChanges = {
|
||||
async name(record) {
|
||||
record.float_field = 132;
|
||||
},
|
||||
};
|
||||
onRpc("onchange", async () => {
|
||||
await def;
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary"/>
|
||||
<field name="name"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name='name'] input").edit("test", { confirm: "blur" });
|
||||
await contains(".o_field_widget[name='float_field'] input").edit("1", { confirm: false });
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_field_monetary .o_monetary_ghost_value").toHaveText("1");
|
||||
});
|
||||
|
||||
test("with 'hide_trailing_zeros' option", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 5,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="monetary" options="{'hide_trailing_zeros': true}"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget input").toHaveValue("9.1");
|
||||
expect(".o_field_widget .o_input span:eq(0)").toHaveText("$");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,293 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { click, keyDown, pointerDown, queryAll, queryFirst } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineModels,
|
||||
defineParams,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
mountWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { useNumpadDecimal } from "@web/views/fields/numpad_decimal_hook";
|
||||
|
||||
class Partner extends models.Model {
|
||||
int_field = fields.Integer();
|
||||
qux = fields.Float({ digits: [16, 1] });
|
||||
currency_id = fields.Many2one({ relation: "currency" });
|
||||
float_factor_field = fields.Float();
|
||||
percentage = fields.Float();
|
||||
monetary = fields.Monetary({ currency_field: "" });
|
||||
progressbar = fields.Integer();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
int_field: 10,
|
||||
qux: 0.44444,
|
||||
float_factor_field: 9.99,
|
||||
percentage: 0.99,
|
||||
monetary: 9.99,
|
||||
currency_id: 1,
|
||||
progressbar: 69,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
class Currency extends models.Model {
|
||||
digits = fields.Float();
|
||||
symbol = fields.Char();
|
||||
position = fields.Char();
|
||||
|
||||
_records = [{ id: 1, display_name: "$", symbol: "$", position: "before" }];
|
||||
}
|
||||
|
||||
defineModels([Partner, Currency]);
|
||||
|
||||
beforeEach(() => {
|
||||
defineParams({ lang_parameters: { decimal_point: ",", thousands_sep: "." } });
|
||||
});
|
||||
|
||||
test("Numeric fields: fields with keydown on numpad decimal key", async () => {
|
||||
defineParams({ lang_parameters: { decimal_point: "🇧🇪" } });
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}" widget="float_factor"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage" widget="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Dispatch numpad "dot" and numpad "comma" keydown events to all inputs and check
|
||||
// Numpad "comma" is specific to some countries (Brazil...)
|
||||
await click(".o_field_float_factor input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_float_factor input").toHaveValue("5🇧🇪00🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_float input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_float input").toHaveValue("0🇧🇪4🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_integer input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_integer input").toHaveValue("10🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_monetary input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_monetary input").toHaveValue("9🇧🇪99🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_percentage input");
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_percentage input").toHaveValue("99🇧🇪🇧🇪");
|
||||
|
||||
await click(".o_field_progressbar input");
|
||||
await animationFrame();
|
||||
|
||||
await keyDown("ArrowRight", { code: "ArrowRight" });
|
||||
await keyDown(".", { code: "NumpadDecimal" });
|
||||
await keyDown(",", { code: "NumpadDecimal" });
|
||||
await animationFrame();
|
||||
expect(".o_field_progressbar input").toHaveValue("0🇧🇪44🇧🇪🇧🇪");
|
||||
});
|
||||
|
||||
test("Numeric fields: NumpadDecimal key is different from the decimalPoint", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}" widget="float_factor"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage" widget="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Get all inputs
|
||||
const floatFactorField = queryFirst(".o_field_float_factor input");
|
||||
const floatInput = queryFirst(".o_field_float input");
|
||||
const integerInput = queryFirst(".o_field_integer input");
|
||||
const monetaryInput = queryFirst(".o_field_monetary input");
|
||||
const percentageInput = queryFirst(".o_field_percentage input");
|
||||
const progressbarInput = queryFirst(".o_field_progressbar input");
|
||||
|
||||
/**
|
||||
* Common assertion steps are extracted in this procedure.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {HTMLInputElement} params.el
|
||||
* @param {[number, number]} params.selectionRange
|
||||
* @param {string} params.expectedValue
|
||||
* @param {string} params.msg
|
||||
*/
|
||||
async function testInputElementOnNumpadDecimal(params) {
|
||||
const { el, selectionRange, expectedValue, msg } = params;
|
||||
|
||||
await pointerDown(el);
|
||||
await animationFrame();
|
||||
el.setSelectionRange(...selectionRange);
|
||||
const [event] = await keyDown(".", { code: "NumpadDecimal" });
|
||||
if (event.defaultPrevented) {
|
||||
expect.step("preventDefault");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
// dispatch an extra keydown event and expect that it's not default prevented
|
||||
const [extraEvent] = await keyDown("1", { code: "Digit1" });
|
||||
if (extraEvent.defaultPrevented) {
|
||||
throw new Error("should not be default prevented");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
// Selection range should be at +2 from the specified selection start (separator + character).
|
||||
expect(el.selectionStart).toBe(selectionRange[0] + 2);
|
||||
expect(el.selectionEnd).toBe(selectionRange[0] + 2);
|
||||
await animationFrame();
|
||||
// NumpadDecimal event should be default prevented
|
||||
expect.verifySteps(["preventDefault"]);
|
||||
expect(el).toHaveValue(expectedValue, { message: msg });
|
||||
}
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatFactorField,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "5,10",
|
||||
msg: "Float factor field from 5,00 to 5,10",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatInput,
|
||||
selectionRange: [0, 2],
|
||||
expectedValue: ",14",
|
||||
msg: "Float field from 0,4 to ,14",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: integerInput,
|
||||
selectionRange: [1, 2],
|
||||
expectedValue: "1,1",
|
||||
msg: "Integer field from 10 to 1,1",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: monetaryInput,
|
||||
selectionRange: [0, 3],
|
||||
expectedValue: ",19",
|
||||
msg: "Monetary field from 9,99 to ,19",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: percentageInput,
|
||||
selectionRange: [1, 1],
|
||||
expectedValue: "9,19",
|
||||
msg: "Percentage field from 99 to 9,19",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: progressbarInput,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "0,14",
|
||||
msg: "Progressbar field 2 from 0,44 to 0,14",
|
||||
});
|
||||
});
|
||||
|
||||
test("useNumpadDecimal should synchronize handlers on input elements", async () => {
|
||||
/**
|
||||
* Takes an array of input elements and asserts that each has the correct event listener.
|
||||
* @param {HTMLInputElement[]} inputEls
|
||||
*/
|
||||
async function testInputElements(inputEls) {
|
||||
for (const inputEl of inputEls) {
|
||||
await pointerDown(inputEl);
|
||||
await animationFrame();
|
||||
const [event] = await keyDown(".", { code: "NumpadDecimal" });
|
||||
if (event.defaultPrevented) {
|
||||
expect.step("preventDefault");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
// dispatch an extra keydown event and expect that it's not default prevented
|
||||
const [extraEvent] = await keyDown("1", { code: "Digit1" });
|
||||
if (extraEvent.defaultPrevented) {
|
||||
throw new Error("should not be default prevented");
|
||||
}
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["preventDefault"]);
|
||||
}
|
||||
}
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<main t-ref="numpadDecimal">
|
||||
<input type="text" placeholder="input 1" />
|
||||
<input t-if="state.showOtherInput" type="text" placeholder="input 2" />
|
||||
</main>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useNumpadDecimal();
|
||||
this.state = useState({ showOtherInput: false });
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mountWithCleanup(MyComponent);
|
||||
await animationFrame();
|
||||
|
||||
// Initially, only one input should be rendered.
|
||||
expect("main > input").toHaveCount(1);
|
||||
await testInputElements(queryAll("main > input"));
|
||||
|
||||
// We show the second input by manually updating the state.
|
||||
comp.state.showOtherInput = true;
|
||||
await animationFrame();
|
||||
|
||||
// The second input should also be able to handle numpad decimal.
|
||||
expect("main > input").toHaveCount(2);
|
||||
await testInputElements(queryAll("main > input"));
|
||||
});
|
||||
|
||||
test("select all content on focus", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="monetary"/></form>`,
|
||||
});
|
||||
|
||||
const input = queryFirst(".o_field_widget[name='monetary'] input");
|
||||
await pointerDown(input);
|
||||
await animationFrame();
|
||||
expect(input.selectionStart).toBe(0);
|
||||
expect(input.selectionEnd).toBe(4);
|
||||
});
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { useNumpadDecimal } from "@web/views/fields/numpad_decimal_hook";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
|
||||
const { Component, mount, useState, xml } = owl;
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
|
||||
currency_id: {
|
||||
string: "Currency",
|
||||
type: "many2one",
|
||||
relation: "currency",
|
||||
searchable: true,
|
||||
},
|
||||
float_factor_field: {
|
||||
string: "Float Factor",
|
||||
type: "float_factor",
|
||||
},
|
||||
percentage: {
|
||||
string: "Percentage",
|
||||
type: "percentage",
|
||||
},
|
||||
monetary: { string: "Monetary", type: "monetary" },
|
||||
progressbar: {
|
||||
type: "integer",
|
||||
},
|
||||
progressmax: {
|
||||
type: "float",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
int_field: 10,
|
||||
qux: 0.44444,
|
||||
float_factor_field: 9.99,
|
||||
percentage: 0.99,
|
||||
monetary: 9.99,
|
||||
currency_id: 1,
|
||||
progressbar: 69,
|
||||
progressmax: 5.41,
|
||||
},
|
||||
],
|
||||
},
|
||||
currency: {
|
||||
fields: {
|
||||
digits: { string: "Digits" },
|
||||
symbol: { string: "Currency Sumbol", type: "char", searchable: true },
|
||||
position: { string: "Currency Position", type: "char", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "$",
|
||||
symbol: "$",
|
||||
position: "before",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
});
|
||||
|
||||
QUnit.module("Numeric fields");
|
||||
|
||||
QUnit.test(
|
||||
"Numeric fields: fields with keydown on numpad decimal key",
|
||||
async function (assert) {
|
||||
registry.category("services").remove("localization");
|
||||
registry
|
||||
.category("services")
|
||||
.add("localization", makeFakeLocalizationService({ decimalPoint: "🇧🇪" }));
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Get all inputs
|
||||
const floatFactorField = target.querySelector(".o_field_float_factor input");
|
||||
const floatInput = target.querySelector(".o_field_float input");
|
||||
const integerInput = target.querySelector(".o_field_integer input");
|
||||
const monetaryInput = target.querySelector(".o_field_monetary input");
|
||||
const percentageInput = target.querySelector(".o_field_percentage input");
|
||||
const progressbarInput = target.querySelector(".o_field_progressbar input");
|
||||
|
||||
// Dispatch numpad "dot" and numpad "comma" keydown events to all inputs and check
|
||||
// Numpad "comma" is specific to some countries (Brazil...)
|
||||
floatFactorField.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
floatFactorField.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(floatFactorField.value, "5🇧🇪00🇧🇪🇧🇪");
|
||||
|
||||
floatInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
floatInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(floatInput.value, "0🇧🇪4🇧🇪🇧🇪");
|
||||
|
||||
integerInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
integerInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(integerInput.value, "10🇧🇪🇧🇪");
|
||||
|
||||
monetaryInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
monetaryInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(monetaryInput.value, "9🇧🇪99🇧🇪🇧🇪");
|
||||
|
||||
percentageInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
percentageInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(percentageInput.value, "99🇧🇪🇧🇪");
|
||||
|
||||
progressbarInput.focus();
|
||||
await nextTick();
|
||||
|
||||
// When the input is focused, we get the length of the input value to be
|
||||
// able to set the cursor position at the end of the value.
|
||||
const length = progressbarInput.value.length;
|
||||
|
||||
// Make sure that the cursor position is at the end of the value.
|
||||
progressbarInput.setSelectionRange(length, length);
|
||||
progressbarInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "." })
|
||||
);
|
||||
progressbarInput.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { code: "NumpadDecimal", key: "," })
|
||||
);
|
||||
await nextTick();
|
||||
assert.strictEqual(progressbarInput.value, "0🇧🇪44🇧🇪🇧🇪");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Numeric fields: NumpadDecimal key is different from the decimalPoint",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /*xml*/ `
|
||||
<form>
|
||||
<field name="float_factor_field" options="{'factor': 0.5}"/>
|
||||
<field name="qux"/>
|
||||
<field name="int_field"/>
|
||||
<field name="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="percentage"/>
|
||||
<field name="progressbar" widget="progressbar" options="{'editable': true, 'max_value': 'qux', 'edit_max_value': true}"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// Get all inputs
|
||||
const floatFactorField = target.querySelector(".o_field_float_factor input");
|
||||
const floatInput = target.querySelector(".o_field_float input");
|
||||
const integerInput = target.querySelector(".o_field_integer input");
|
||||
const monetaryInput = target.querySelector(".o_field_monetary input");
|
||||
const percentageInput = target.querySelector(".o_field_percentage input");
|
||||
const progressbarInput = target.querySelector(".o_field_progressbar input");
|
||||
|
||||
/**
|
||||
* Common assertion steps are extracted in this procedure.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {HTMLInputElement} params.el
|
||||
* @param {[number, number]} params.selectionRange
|
||||
* @param {string} params.expectedValue
|
||||
* @param {string} params.msg
|
||||
*/
|
||||
async function testInputElementOnNumpadDecimal(params) {
|
||||
const { el, selectionRange, expectedValue, msg } = params;
|
||||
|
||||
el.focus();
|
||||
await nextTick();
|
||||
|
||||
el.setSelectionRange(...selectionRange);
|
||||
const numpadDecimalEvent = new KeyboardEvent("keydown", {
|
||||
code: "NumpadDecimal",
|
||||
key: ".",
|
||||
});
|
||||
numpadDecimalEvent.preventDefault = () => assert.step("preventDefault");
|
||||
el.dispatchEvent(numpadDecimalEvent);
|
||||
await nextTick();
|
||||
|
||||
// dispatch an extra keydown event and assert that it's not default prevented
|
||||
const extraEvent = new KeyboardEvent("keydown", { code: "Digit1", key: "1" });
|
||||
extraEvent.preventDefault = () => {
|
||||
throw new Error("should not be default prevented");
|
||||
};
|
||||
el.dispatchEvent(extraEvent);
|
||||
await nextTick();
|
||||
|
||||
// Selection range should be at 1 + the specified selection start.
|
||||
assert.strictEqual(el.selectionStart, selectionRange[0] + 1);
|
||||
assert.strictEqual(el.selectionEnd, selectionRange[0] + 1);
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["preventDefault"],
|
||||
"NumpadDecimal event should be default prevented"
|
||||
);
|
||||
assert.strictEqual(el.value, expectedValue, msg);
|
||||
}
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatFactorField,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "5,0",
|
||||
msg: "Float factor field from 5,00 to 5,0",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: floatInput,
|
||||
selectionRange: [0, 2],
|
||||
expectedValue: ",4",
|
||||
msg: "Float field from 0,4 to ,4",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: integerInput,
|
||||
selectionRange: [1, 2],
|
||||
expectedValue: "1,",
|
||||
msg: "Integer field from 10 to 1,",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: monetaryInput,
|
||||
selectionRange: [0, 3],
|
||||
expectedValue: ",9",
|
||||
msg: "Monetary field from 9,99 to ,9",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: percentageInput,
|
||||
selectionRange: [1, 1],
|
||||
expectedValue: "9,9",
|
||||
msg: "Percentage field from 99 to 9,9",
|
||||
});
|
||||
|
||||
await testInputElementOnNumpadDecimal({
|
||||
el: progressbarInput,
|
||||
selectionRange: [1, 3],
|
||||
expectedValue: "0,4",
|
||||
msg: "Progressbar field 2 from 0,44 to 0,4",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"useNumpadDecimal should synchronize handlers on input elements",
|
||||
async function (assert) {
|
||||
/**
|
||||
* Takes an array of input elements and asserts that each has the correct event listener.
|
||||
* @param {HTMLInputElement[]} inputEls
|
||||
*/
|
||||
async function testInputElements(inputEls) {
|
||||
for (const inputEl of inputEls) {
|
||||
inputEl.focus();
|
||||
const numpadDecimalEvent = new KeyboardEvent("keydown", {
|
||||
code: "NumpadDecimal",
|
||||
key: ".",
|
||||
});
|
||||
numpadDecimalEvent.preventDefault = () => assert.step("preventDefault");
|
||||
inputEl.dispatchEvent(numpadDecimalEvent);
|
||||
await nextTick();
|
||||
|
||||
// dispatch an extra keydown event and assert that it's not default prevented
|
||||
const extraEvent = new KeyboardEvent("keydown", { code: "Digit1", key: "1" });
|
||||
extraEvent.preventDefault = () => {
|
||||
throw new Error("should not be default prevented");
|
||||
};
|
||||
inputEl.dispatchEvent(extraEvent);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["preventDefault"]);
|
||||
}
|
||||
}
|
||||
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useNumpadDecimal();
|
||||
this.state = useState({ showOtherInput: false });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<main t-ref="numpadDecimal">
|
||||
<input type="text" placeholder="input 1" />
|
||||
<input t-if="state.showOtherInput" type="text" placeholder="input 2" />
|
||||
</main>
|
||||
`;
|
||||
const comp = await mount(MyComponent, target, { env: await makeTestEnv() });
|
||||
|
||||
// Initially, only one input should be rendered.
|
||||
assert.containsOnce(target, "main > input");
|
||||
await testInputElements(target.querySelectorAll("main > input"));
|
||||
|
||||
// We show the second input by manually updating the state.
|
||||
comp.state.showOtherInput = true;
|
||||
await nextTick();
|
||||
|
||||
// The second input should also be able to handle numpad decimal.
|
||||
assert.containsN(target, "main > input", 2);
|
||||
await testInputElements(target.querySelectorAll("main > input"));
|
||||
}
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,177 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { makeMockEnv, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { nbsp } from "@web/core/utils/strings";
|
||||
import {
|
||||
parseFloat,
|
||||
parseFloatTime,
|
||||
parseInteger,
|
||||
parseMonetary,
|
||||
parsePercentage,
|
||||
} from "@web/views/fields/parsers";
|
||||
|
||||
beforeEach(makeMockEnv);
|
||||
|
||||
test("parseFloat", () => {
|
||||
expect(parseFloat("")).toBe(0);
|
||||
expect(parseFloat("0")).toBe(0);
|
||||
expect(parseFloat("100.00")).toBe(100);
|
||||
expect(parseFloat("-100.00")).toBe(-100);
|
||||
expect(parseFloat("1,000.00")).toBe(1000);
|
||||
expect(parseFloat("1,000,000.00")).toBe(1000000);
|
||||
expect(parseFloat("1,234.567")).toBe(1234.567);
|
||||
expect(() => parseFloat("1.000.000")).toThrow();
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
expect(parseFloat("1.234,567")).toBe(1234.567);
|
||||
|
||||
// Can evaluate expression from locale with decimal point different from ".".
|
||||
expect(parseFloat("=1.000,1 + 2.000,2")).toBe(3000.3);
|
||||
expect(parseFloat("=1.000,00 + 11.121,00")).toBe(12121);
|
||||
expect(parseFloat("=1000,00 + 11122,00")).toBe(12122);
|
||||
expect(parseFloat("=1000 + 11123")).toBe(12123);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
expect(parseFloat("1234,567")).toBe(1234.567);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: nbsp });
|
||||
expect(parseFloat("9 876,543")).toBe(9876.543);
|
||||
expect(parseFloat("1 234 567,89")).toBe(1234567.89);
|
||||
expect(parseFloat(`98${nbsp}765 432,1`)).toBe(98765432.1);
|
||||
});
|
||||
|
||||
test("parseFloatTime", () => {
|
||||
expect(parseFloatTime("0")).toBe(0);
|
||||
expect(parseFloatTime("100")).toBe(100);
|
||||
expect(parseFloatTime("100.00")).toBe(100);
|
||||
expect(parseFloatTime("7:15")).toBe(7.25);
|
||||
expect(parseFloatTime("-4:30")).toBe(-4.5);
|
||||
expect(parseFloatTime(":")).toBe(0);
|
||||
expect(parseFloatTime("1:")).toBe(1);
|
||||
expect(parseFloatTime(":12")).toBe(0.2);
|
||||
|
||||
expect(() => parseFloatTime("a:1")).toThrow();
|
||||
expect(() => parseFloatTime("1:a")).toThrow();
|
||||
expect(() => parseFloatTime("1:1:")).toThrow();
|
||||
expect(() => parseFloatTime(":1:1")).toThrow();
|
||||
});
|
||||
|
||||
test("parseInteger", () => {
|
||||
expect(parseInteger("")).toBe(0);
|
||||
expect(parseInteger("0")).toBe(0);
|
||||
expect(parseInteger("100")).toBe(100);
|
||||
expect(parseInteger("-100")).toBe(-100);
|
||||
expect(parseInteger("1,000")).toBe(1000);
|
||||
expect(parseInteger("1,000,000")).toBe(1000000);
|
||||
expect(parseInteger("-2,147,483,648")).toBe(-2147483648);
|
||||
expect(parseInteger("2,147,483,647")).toBe(2147483647);
|
||||
expect(() => parseInteger("1.000.000")).toThrow();
|
||||
expect(() => parseInteger("1,234.567")).toThrow();
|
||||
expect(() => parseInteger("-2,147,483,649")).toThrow();
|
||||
expect(() => parseInteger("2,147,483,648")).toThrow();
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
expect(parseInteger("1.000.000")).toBe(1000000);
|
||||
expect(() => parseInteger("1.234,567")).toThrow();
|
||||
// fallback to en localization
|
||||
expect(parseInteger("1,000,000")).toBe(1000000);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
expect(parseInteger("1000000")).toBe(1000000);
|
||||
});
|
||||
|
||||
test("parsePercentage", () => {
|
||||
expect(parsePercentage("")).toBe(0);
|
||||
expect(parsePercentage("0")).toBe(0);
|
||||
expect(parsePercentage("0.5")).toBe(0.005);
|
||||
expect(parsePercentage("1")).toBe(0.01);
|
||||
expect(parsePercentage("100")).toBe(1);
|
||||
expect(parsePercentage("50%")).toBe(0.5);
|
||||
expect(() => parsePercentage("50%40")).toThrow();
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
expect(parsePercentage("1.234,56")).toBe(12.3456);
|
||||
expect(parsePercentage("6,02")).toBe(0.0602);
|
||||
});
|
||||
|
||||
test("parsers fallback on english localisation", () => {
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ",",
|
||||
thousandsSep: ".",
|
||||
});
|
||||
|
||||
expect(parseInteger("1,000,000")).toBe(1000000);
|
||||
expect(parseFloat("1,000,000.50")).toBe(1000000.5);
|
||||
});
|
||||
|
||||
test("parseMonetary", () => {
|
||||
expect(parseMonetary("")).toBe(0);
|
||||
expect(parseMonetary("0")).toBe(0);
|
||||
expect(parseMonetary("100.00\u00a0€")).toBe(100);
|
||||
expect(parseMonetary("-100.00")).toBe(-100);
|
||||
expect(parseMonetary("1,000.00")).toBe(1000);
|
||||
expect(parseMonetary(".1")).toBe(0.1);
|
||||
expect(parseMonetary("1,000,000.00")).toBe(1000000);
|
||||
expect(parseMonetary("$\u00a0125.00")).toBe(125);
|
||||
expect(parseMonetary("1,000.00\u00a0€")).toBe(1000);
|
||||
|
||||
expect(parseMonetary("\u00a0")).toBe(0);
|
||||
expect(parseMonetary("1\u00a0")).toBe(1);
|
||||
expect(parseMonetary("\u00a01")).toBe(1);
|
||||
|
||||
expect(parseMonetary("12.00 €")).toBe(12);
|
||||
expect(parseMonetary("$ 12.00")).toBe(12);
|
||||
expect(parseMonetary("1\u00a0$")).toBe(1);
|
||||
expect(parseMonetary("$\u00a01")).toBe(1);
|
||||
|
||||
expect(() => parseMonetary("1$\u00a01")).toThrow();
|
||||
expect(() => parseMonetary("$\u00a012.00\u00a034")).toThrow();
|
||||
|
||||
// nbsp as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: "\u00a0", decimalPoint: "," });
|
||||
expect(parseMonetary("1\u00a0000,06\u00a0€")).toBe(1000.06);
|
||||
expect(parseMonetary("$\u00a01\u00a0000,07")).toBe(1000.07);
|
||||
expect(parseMonetary("1000000,08")).toBe(1000000.08);
|
||||
expect(parseMonetary("$ -1\u00a0000,09")).toBe(-1000.09);
|
||||
|
||||
// symbol not separated from the value
|
||||
expect(parseMonetary("1\u00a0000,08€")).toBe(1000.08);
|
||||
expect(parseMonetary("€1\u00a0000,09")).toBe(1000.09);
|
||||
expect(parseMonetary("$1\u00a0000,10")).toBe(1000.1);
|
||||
expect(parseMonetary("$-1\u00a0000,11")).toBe(-1000.11);
|
||||
|
||||
// any symbol
|
||||
expect(parseMonetary("1\u00a0000,11EUROS")).toBe(1000.11);
|
||||
expect(parseMonetary("EUR1\u00a0000,12")).toBe(1000.12);
|
||||
expect(parseMonetary("DOL1\u00a0000,13")).toBe(1000.13);
|
||||
expect(parseMonetary("1\u00a0000,14DOLLARS")).toBe(1000.14);
|
||||
expect(parseMonetary("DOLLARS+1\u00a0000,15")).toBe(1000.15);
|
||||
expect(parseMonetary("EURO-1\u00a0000,16DOGE")).toBe(-1000.16);
|
||||
|
||||
// comma as decimal point and dot as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: ".", decimalPoint: "," });
|
||||
expect(parseMonetary("10,08")).toBe(10.08);
|
||||
expect(parseMonetary("")).toBe(0);
|
||||
expect(parseMonetary("0")).toBe(0);
|
||||
expect(parseMonetary("100,12\u00a0€")).toBe(100.12);
|
||||
expect(parseMonetary("-100,12")).toBe(-100.12);
|
||||
expect(parseMonetary("1.000,12")).toBe(1000.12);
|
||||
expect(parseMonetary(",1")).toBe(0.1);
|
||||
expect(parseMonetary("1.000.000,12")).toBe(1000000.12);
|
||||
expect(parseMonetary("$\u00a0125,12")).toBe(125.12);
|
||||
expect(parseMonetary("1.000,00\u00a0€")).toBe(1000);
|
||||
expect(parseMonetary(",")).toBe(0);
|
||||
expect(parseMonetary("1\u00a0")).toBe(1);
|
||||
expect(parseMonetary("\u00a01")).toBe(1);
|
||||
expect(parseMonetary("12,34 €")).toBe(12.34);
|
||||
expect(parseMonetary("$ 12,34")).toBe(12.34);
|
||||
|
||||
// Can evaluate expression
|
||||
expect(parseMonetary("=1.000,1 + 2.000,2")).toBe(3000.3);
|
||||
expect(parseMonetary("=1.000,00 + 11.121,00")).toBe(12121);
|
||||
expect(parseMonetary("=1000,00 + 11122,00")).toBe(12122);
|
||||
expect(parseMonetary("=1000 + 11123")).toBe(12123);
|
||||
});
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import {
|
||||
parseFloat,
|
||||
parseFloatTime,
|
||||
parseInteger,
|
||||
parsePercentage,
|
||||
parseMonetary,
|
||||
} from "@web/views/fields/parsers";
|
||||
import { session } from "@web/session";
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { nbsp } from "@web/core/utils/strings";
|
||||
|
||||
function expectInvalidNumberError(assert, func, value, options) {
|
||||
let message = `${func.name} fails on value: "${value}"`;
|
||||
if (options) {
|
||||
message += ` with options: ${JSON.stringify(options)}`;
|
||||
}
|
||||
assert.throws(() => func(value, options), message);
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(localization, defaultLocalization);
|
||||
});
|
||||
|
||||
QUnit.module("Parsers");
|
||||
|
||||
QUnit.test("parseFloat", async (assert) => {
|
||||
assert.strictEqual(parseFloat(""), 0);
|
||||
assert.strictEqual(parseFloat("0"), 0);
|
||||
assert.strictEqual(parseFloat("100.00"), 100);
|
||||
assert.strictEqual(parseFloat("-100.00"), -100);
|
||||
assert.strictEqual(parseFloat("1,000.00"), 1000);
|
||||
assert.strictEqual(parseFloat("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(parseFloat("1,234.567"), 1234.567);
|
||||
expectInvalidNumberError(assert, parseFloat, "1.000.000");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
assert.strictEqual(parseFloat("1.234,567"), 1234.567);
|
||||
|
||||
// Can evaluate expression from locale with decimal point different from ".".
|
||||
assert.strictEqual(parseFloat("=1.000,1 + 2.000,2"), 3000.3);
|
||||
assert.strictEqual(parseFloat("=1.000,00 + 11.121,00"), 12121);
|
||||
assert.strictEqual(parseFloat("=1000,00 + 11122,00"), 12122);
|
||||
assert.strictEqual(parseFloat("=1000 + 11123"), 12123);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
assert.strictEqual(parseFloat("1234,567"), 1234.567);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: nbsp });
|
||||
assert.strictEqual(parseFloat("9 876,543"), 9876.543);
|
||||
assert.strictEqual(parseFloat("1 234 567,89"), 1234567.89);
|
||||
assert.strictEqual(parseFloat(`98${nbsp}765 432,1`), 98765432.1);
|
||||
});
|
||||
|
||||
QUnit.test("parseFloatTime", function (assert) {
|
||||
assert.strictEqual(parseFloatTime("0"), 0);
|
||||
assert.strictEqual(parseFloatTime("100"), 100);
|
||||
assert.strictEqual(parseFloatTime("100.00"), 100);
|
||||
assert.strictEqual(parseFloatTime("7:15"), 7.25);
|
||||
assert.strictEqual(parseFloatTime("-4:30"), -4.5);
|
||||
assert.strictEqual(parseFloatTime(":"), 0);
|
||||
assert.strictEqual(parseFloatTime("1:"), 1);
|
||||
assert.strictEqual(parseFloatTime(":12"), 0.2);
|
||||
|
||||
expectInvalidNumberError(assert, parseFloatTime, "a:1");
|
||||
expectInvalidNumberError(assert, parseFloatTime, "1:a");
|
||||
expectInvalidNumberError(assert, parseFloatTime, "1:1:");
|
||||
expectInvalidNumberError(assert, parseFloatTime, ":1:1");
|
||||
});
|
||||
|
||||
QUnit.test("parseInteger", function (assert) {
|
||||
assert.strictEqual(parseInteger(""), 0);
|
||||
assert.strictEqual(parseInteger("0"), 0);
|
||||
assert.strictEqual(parseInteger("100"), 100);
|
||||
assert.strictEqual(parseInteger("-100"), -100);
|
||||
assert.strictEqual(parseInteger("1,000"), 1000);
|
||||
assert.strictEqual(parseInteger("1,000,000"), 1000000);
|
||||
assert.strictEqual(parseInteger("-2,147,483,648"), -2147483648);
|
||||
assert.strictEqual(parseInteger("2,147,483,647"), 2147483647);
|
||||
expectInvalidNumberError(assert, parseInteger, "1.000.000");
|
||||
expectInvalidNumberError(assert, parseInteger, "1,234.567");
|
||||
expectInvalidNumberError(assert, parseInteger, "-2,147,483,649");
|
||||
expectInvalidNumberError(assert, parseInteger, "2,147,483,648");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
assert.strictEqual(parseInteger("1.000.000"), 1000000);
|
||||
expectInvalidNumberError(assert, parseInteger, "1.234,567");
|
||||
// fallback to en localization
|
||||
assert.strictEqual(parseInteger("1,000,000"), 1000000);
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: false });
|
||||
assert.strictEqual(parseInteger("1000000"), 1000000);
|
||||
});
|
||||
|
||||
QUnit.test("parsePercentage", function (assert) {
|
||||
assert.strictEqual(parsePercentage(""), 0);
|
||||
assert.strictEqual(parsePercentage("0"), 0);
|
||||
assert.strictEqual(parsePercentage("0.5"), 0.005);
|
||||
assert.strictEqual(parsePercentage("1"), 0.01);
|
||||
assert.strictEqual(parsePercentage("100"), 1);
|
||||
assert.strictEqual(parsePercentage("50%"), 0.5);
|
||||
expectInvalidNumberError(assert, parsePercentage, "50%40");
|
||||
|
||||
patchWithCleanup(localization, { decimalPoint: ",", thousandsSep: "." });
|
||||
|
||||
assert.strictEqual(parsePercentage("1.234,56"), 12.3456);
|
||||
assert.strictEqual(parsePercentage("6,02"), 0.0602);
|
||||
});
|
||||
|
||||
QUnit.test("parseMonetary", function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
currencies: {
|
||||
1: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
3: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(parseMonetary(""), 0);
|
||||
assert.strictEqual(parseMonetary("0"), 0);
|
||||
assert.strictEqual(parseMonetary("100.00\u00a0€"), 100);
|
||||
assert.strictEqual(parseMonetary("-100.00"), -100);
|
||||
assert.strictEqual(parseMonetary("1,000.00"), 1000);
|
||||
assert.strictEqual(parseMonetary(".1"), 0.1);
|
||||
assert.strictEqual(parseMonetary("1,000,000.00"), 1000000);
|
||||
assert.strictEqual(parseMonetary("$\u00a0125.00"), 125);
|
||||
assert.strictEqual(parseMonetary("1,000.00\u00a0€"), 1000);
|
||||
|
||||
assert.strictEqual(parseMonetary("\u00a0"), 0);
|
||||
assert.strictEqual(parseMonetary("1\u00a0"), 1);
|
||||
assert.strictEqual(parseMonetary("\u00a01"), 1);
|
||||
|
||||
assert.strictEqual(parseMonetary("12.00 €"), 12);
|
||||
assert.strictEqual(parseMonetary("$ 12.00"), 12);
|
||||
assert.strictEqual(parseMonetary("1\u00a0$"), 1);
|
||||
assert.strictEqual(parseMonetary("$\u00a01"), 1);
|
||||
|
||||
assert.throws(() => parseMonetary("1$\u00a01"));
|
||||
assert.throws(() => parseMonetary("$\u00a012.00\u00a034"));
|
||||
|
||||
// nbsp as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: "\u00a0", decimalPoint: "," });
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,06\u00a0€"), 1000.06);
|
||||
assert.strictEqual(parseMonetary("$\u00a01\u00a0000,07"), 1000.07);
|
||||
assert.strictEqual(parseMonetary("1000000,08"), 1000000.08);
|
||||
assert.strictEqual(parseMonetary("$ -1\u00a0000,09"), -1000.09);
|
||||
|
||||
// symbol not separated from the value
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,08€"), 1000.08);
|
||||
assert.strictEqual(parseMonetary("€1\u00a0000,09"), 1000.09);
|
||||
assert.strictEqual(parseMonetary("$1\u00a0000,10"), 1000.1);
|
||||
assert.strictEqual(parseMonetary("$-1\u00a0000,11"), -1000.11);
|
||||
|
||||
// any symbol
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,11EUROS"), 1000.11);
|
||||
assert.strictEqual(parseMonetary("EUR1\u00a0000,12"), 1000.12);
|
||||
assert.strictEqual(parseMonetary("DOL1\u00a0000,13"), 1000.13);
|
||||
assert.strictEqual(parseMonetary("1\u00a0000,14DOLLARS"), 1000.14);
|
||||
assert.strictEqual(parseMonetary("DOLLARS+1\u00a0000,15"), 1000.15);
|
||||
assert.strictEqual(parseMonetary("EURO-1\u00a0000,16DOGE"), -1000.16);
|
||||
|
||||
// comma as decimal point and dot as thousands separator
|
||||
patchWithCleanup(localization, { thousandsSep: ".", decimalPoint: "," });
|
||||
assert.strictEqual(parseMonetary("10,08"), 10.08);
|
||||
assert.strictEqual(parseMonetary(""), 0);
|
||||
assert.strictEqual(parseMonetary("0"), 0);
|
||||
assert.strictEqual(parseMonetary("100,12\u00a0€"), 100.12);
|
||||
assert.strictEqual(parseMonetary("-100,12"), -100.12);
|
||||
assert.strictEqual(parseMonetary("1.000,12"), 1000.12);
|
||||
assert.strictEqual(parseMonetary(",1"), 0.1);
|
||||
assert.strictEqual(parseMonetary("1.000.000,12"), 1000000.12);
|
||||
assert.strictEqual(parseMonetary("$\u00a0125,12"), 125.12);
|
||||
assert.strictEqual(parseMonetary("1.000,00\u00a0€"), 1000);
|
||||
assert.strictEqual(parseMonetary(","), 0);
|
||||
assert.strictEqual(parseMonetary("1\u00a0"), 1);
|
||||
assert.strictEqual(parseMonetary("\u00a01"), 1);
|
||||
assert.strictEqual(parseMonetary("12,34 €"), 12.34);
|
||||
assert.strictEqual(parseMonetary("$ 12,34"), 12.34);
|
||||
|
||||
// Can evaluate expression
|
||||
assert.strictEqual(parseMonetary("=1.000,1 + 2.000,2"), 3000.3);
|
||||
assert.strictEqual(parseMonetary("=1.000,00 + 11.121,00"), 12121);
|
||||
assert.strictEqual(parseMonetary("=1000,00 + 11122,00"), 12122);
|
||||
assert.strictEqual(parseMonetary("=1000 + 11123"), 12123);
|
||||
});
|
||||
|
||||
QUnit.test("parsers fallback on english localisation", function (assert) {
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ",",
|
||||
thousandsSep: ".",
|
||||
});
|
||||
|
||||
assert.strictEqual(parseInteger("1,000,000"), 1000000);
|
||||
assert.strictEqual(parseFloat("1,000,000.50"), 1000000.5);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { test, expect } from "@odoo/hoot";
|
||||
import { click, setInputFiles, queryOne, waitFor } from "@odoo/hoot-dom";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
const getIframeSrc = () => queryOne(".o_field_widget iframe.o_pdfview_iframe").dataset.src;
|
||||
|
||||
const getIframeProtocol = () => getIframeSrc().match(/\?file=(\w+)%3A/)[1];
|
||||
|
||||
const getIframeViewerParams = () =>
|
||||
decodeURIComponent(getIframeSrc().match(/%2Fweb%2Fcontent%3F(.*)#page/)[1]);
|
||||
|
||||
class Partner extends models.Model {
|
||||
document = fields.Binary({ string: "Binary" });
|
||||
_records = [
|
||||
{
|
||||
document: "coucou==\n",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PdfViewerField without data", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
expect(".o_field_widget").toHaveClass("o_field_pdf_viewer");
|
||||
expect(".o_select_file_button:not(.o_hidden)").toHaveCount(1);
|
||||
expect(".o_pdfview_iframe").toHaveCount(0);
|
||||
expect(`input[type="file"]`).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("PdfViewerField: basic rendering", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
expect(".o_field_widget").toHaveClass("o_field_pdf_viewer");
|
||||
expect(".o_select_file_button").toHaveCount(1);
|
||||
expect(".o_field_widget iframe.o_pdfview_iframe").toHaveCount(1);
|
||||
expect(getIframeProtocol()).toBe("https");
|
||||
expect(getIframeViewerParams()).toBe("model=partner&field=document&id=1");
|
||||
});
|
||||
|
||||
test("PdfViewerField: upload rendering", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1]).toEqual({ document: btoa("test") });
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
expect("iframe.o_pdfview_iframe").toHaveCount(0);
|
||||
const file = new File(["test"], "test.pdf", { type: "application/pdf" });
|
||||
await click(".o_field_pdf_viewer input[type=file]");
|
||||
await setInputFiles(file);
|
||||
await waitFor("iframe.o_pdfview_iframe");
|
||||
expect(getIframeProtocol()).toBe("blob");
|
||||
await clickSave();
|
||||
expect(getIframeProtocol()).toBe("blob");
|
||||
});
|
||||
|
||||
test("PdfViewerField: upload file and download it", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
mockService("action", {
|
||||
doAction({ type }) {
|
||||
expect.step(type);
|
||||
return super.doAction(...arguments);
|
||||
},
|
||||
});
|
||||
patchWithCleanup(browser, {
|
||||
open: (_url, type) => {
|
||||
expect.step(`browser_open:${type}`);
|
||||
},
|
||||
});
|
||||
expect("iframe.o_pdfview_iframe").toHaveCount(1);
|
||||
const file = new File(["test"], "test.pdf", { type: "application/pdf" });
|
||||
await click(".o_field_pdf_viewer input[type=file]");
|
||||
await setInputFiles(file);
|
||||
await waitFor("iframe.o_pdfview_iframe");
|
||||
await clickSave();
|
||||
await click(".fa-download");
|
||||
expect.verifySteps(["ir.actions.act_url", "browser_open:_blank"]);
|
||||
});
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
const getIframe = () => target.querySelector(".o_field_widget iframe.o_pdfview_iframe");
|
||||
|
||||
const getIframeProtocol = () => getIframe().dataset.src.match(/\?file=(\w+)%3A/)[1];
|
||||
|
||||
const getIframeViewerParams = () =>
|
||||
decodeURIComponent(getIframe().dataset.src.match(/%2Fweb%2Fcontent%3F(.*)#page/)[1]);
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
document: { string: "Binary", type: "binary" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
document: "coucou==\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PdfViewerField");
|
||||
|
||||
QUnit.test("PdfViewerField without data", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector(".o_field_widget"), "o_field_pdf_viewer");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_select_file_button:not(.o_hidden)",
|
||||
"there should be a visible 'Upload' button"
|
||||
);
|
||||
assert.containsNone(target, ".o_pdfview_iframe", "there should be no iframe");
|
||||
assert.containsOnce(target, 'input[type="file"]', "there should be one input");
|
||||
});
|
||||
|
||||
QUnit.test("PdfViewerField: basic rendering", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector(".o_field_widget"), "o_field_pdf_viewer");
|
||||
assert.containsOnce(target, ".o_select_file_button", "there should be an 'Upload' button");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_widget iframe.o_pdfview_iframe",
|
||||
"there should be an iframe"
|
||||
);
|
||||
assert.strictEqual(getIframeProtocol(), "http");
|
||||
assert.strictEqual(getIframeViewerParams(), "model=partner&field=document&id=1");
|
||||
});
|
||||
|
||||
QUnit.test("PdfViewerField: upload rendering", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: '<form><field name="document" widget="pdf_viewer"/></form>',
|
||||
async mockRPC(_route, { method, args }) {
|
||||
if (method === "create") {
|
||||
assert.deepEqual(args[0], { document: btoa("test") });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone(target, ".o_pdfview_iframe", "there is no PDF Viewer");
|
||||
|
||||
const file = new File(["test"], "test.pdf", { type: "application/pdf" });
|
||||
await editInput(target, ".o_field_pdf_viewer input[type=file]", file);
|
||||
|
||||
assert.containsOnce(target, ".o_pdfview_iframe", "there is a PDF Viewer");
|
||||
assert.strictEqual(getIframeProtocol(), "blob");
|
||||
|
||||
await clickSave(target);
|
||||
|
||||
assert.strictEqual(getIframeProtocol(), "blob");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
import { test, expect } from "@odoo/hoot";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char({
|
||||
string: "Foo",
|
||||
default: "My little Foo Value",
|
||||
trim: true,
|
||||
});
|
||||
int_field = fields.Integer();
|
||||
float_field = fields.Float();
|
||||
_records = [
|
||||
{ id: 1, foo: "yop", int_field: 10 },
|
||||
{ id: 2, foo: "gnap", int_field: 80 },
|
||||
{ id: 3, foo: "blip", float_field: 33.3333 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PercentPieField in form view with value < 50%", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value").toHaveText("10%", {
|
||||
message: "should have 10% as pie value since int_field=10",
|
||||
});
|
||||
|
||||
expect(
|
||||
queryOne(".o_field_percent_pie.o_field_widget .o_pie").style.background.replaceAll(
|
||||
/\s+/g,
|
||||
" "
|
||||
)
|
||||
).toBe(
|
||||
"conic-gradient( var(--PercentPieField-color-active) 0% 10%, var(--PercentPieField-color-static) 0% 100% )",
|
||||
{ message: "pie should have a background computed for its value of 10%" }
|
||||
);
|
||||
});
|
||||
|
||||
test("PercentPieField in form view with value > 50%", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value").toHaveText("80%", {
|
||||
message: "should have 80% as pie value since int_field=80",
|
||||
});
|
||||
expect(
|
||||
queryOne(".o_field_percent_pie.o_field_widget .o_pie").style.background.replaceAll(
|
||||
/\s+/g,
|
||||
" "
|
||||
)
|
||||
).toBe(
|
||||
"conic-gradient( var(--PercentPieField-color-active) 0% 80%, var(--PercentPieField-color-static) 0% 100% )",
|
||||
{ message: "pie should have a background computed for its value of 80%" }
|
||||
);
|
||||
});
|
||||
|
||||
test("PercentPieField in form view with float value", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="float_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 3,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value").toHaveText("33.33%", {
|
||||
message:
|
||||
"should have 33.33% as pie value since float_field=33.3333 and its value is rounded to 2 decimals",
|
||||
});
|
||||
expect(
|
||||
queryOne(".o_field_percent_pie.o_field_widget .o_pie").style.background.replaceAll(
|
||||
/\s+/g,
|
||||
" "
|
||||
)
|
||||
).toBe(
|
||||
"conic-gradient( var(--PercentPieField-color-active) 0% 33.3333%, var(--PercentPieField-color-static) 0% 100% )",
|
||||
{ message: "pie should have a background computed for its value of 33.3333%" }
|
||||
);
|
||||
});
|
||||
|
||||
test("hide the string when the PercentPieField widget is used in the view", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text").not.toBeVisible();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("show the string when the PercentPieField widget is used in a button with the class oe_stat_button", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<div name="button_box" class="oe_button_box">
|
||||
<button type="object" class="oe_stat_button">
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</button>
|
||||
</div>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie").toHaveCount(1);
|
||||
expect(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text").toBeVisible();
|
||||
});
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {
|
||||
string: "Foo",
|
||||
type: "char",
|
||||
default: "My little Foo Value",
|
||||
searchable: true,
|
||||
trim: true,
|
||||
},
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
float_field: {
|
||||
string: "Float_field",
|
||||
type: "float",
|
||||
digits: [0, 1],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: "yop", int_field: 10 },
|
||||
{ id: 2, foo: "gnap", int_field: 80 },
|
||||
{ id: 3, foo: "dop", float_field: 65.6},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PercentPieField");
|
||||
|
||||
QUnit.test("PercentPieField in form view with value < 50%", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"10%",
|
||||
"should have 10% as pie value since int_field=10"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(180deg)",
|
||||
"left mask should be covering the whole left side of the pie"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1].style
|
||||
.transform,
|
||||
"rotate(36deg)",
|
||||
"right mask should be rotated from 360*(10/100) = 36 degrees"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"10%",
|
||||
"should have 10% as pie value since int_field=10"
|
||||
);
|
||||
assert.ok(
|
||||
_.str.include(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(180deg)"
|
||||
),
|
||||
"left mask should be covering the whole left side of the pie"
|
||||
);
|
||||
assert.ok(
|
||||
_.str.include(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1]
|
||||
.style.transform,
|
||||
"rotate(36deg)"
|
||||
),
|
||||
"right mask should be rotated from 360*(10/100) = 36 degrees"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"10%",
|
||||
"should have 10% as pie value since int_field=10"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(180deg)",
|
||||
"left mask should be covering the whole left side of the pie"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1].style
|
||||
.transform,
|
||||
"rotate(36deg)",
|
||||
"right mask should be rotated from 360*(10/100) = 36 degrees"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PercentPieField in form view with value > 50%", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="int_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"80%",
|
||||
"should have 80% as pie value since int_field=80"
|
||||
);
|
||||
assert.ok(
|
||||
_.str.include(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(288deg)"
|
||||
),
|
||||
"left mask should be rotated from 360*(80/100) = 288 degrees"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1],
|
||||
"o_full",
|
||||
"right mask should be hidden since the value > 50%"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"80%",
|
||||
"should have 80% as pie value since int_field=80"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(288deg)",
|
||||
"left mask should be rotated from 360*(80/100) = 288 degrees"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1],
|
||||
"o_full",
|
||||
"right mask should be hidden since the value > 50%"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_percent_pie.o_field_widget .o_pie",
|
||||
"should have a pie chart"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"80%",
|
||||
"should have 80% as pie value since int_field=80"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_mask").style
|
||||
.transform,
|
||||
"rotate(288deg)",
|
||||
"left mask should be rotated from 360*(80/100) = 288 degrees"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".o_field_percent_pie.o_field_widget .o_pie .o_mask")[1],
|
||||
"o_full",
|
||||
"right mask should be hidden since the value > 50%"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PercentPieField in form view with float value", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="float_field" widget="percentpie"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 3,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
|
||||
.textContent,
|
||||
"66%",
|
||||
"should have 66% as pie value since float_field=65.6"
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: This test would pass without any issue since all the classes and
|
||||
// custom style attributes are correctly set on the widget in list
|
||||
// view, but since the scss itself for this widget currently only
|
||||
// applies inside the form view, the widget is unusable. This test can
|
||||
// be uncommented when we refactor the scss files so that this widget
|
||||
// stylesheet applies in both form and list view.
|
||||
// QUnit.test('percentpie widget in editable list view', async function(assert) {
|
||||
// assert.expect(10);
|
||||
//
|
||||
// var list = await createView({
|
||||
// View: ListView,
|
||||
// model: 'partner',
|
||||
// data: this.data,
|
||||
// arch: '<tree editable="bottom">' +
|
||||
// '<field name="foo"/>' +
|
||||
// '<field name="int_field" widget="percentpie"/>' +
|
||||
// '</tree>',
|
||||
// });
|
||||
//
|
||||
// assert.containsN(list, '.o_field_percent_pie .o_pie', 5,
|
||||
// "should have five pie charts");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_pie_value').textContent,
|
||||
// '10%', "should have 10% as pie value since int_field=10");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').attr('style'),
|
||||
// 'rotate(180deg)', "left mask should be covering the whole left side of the pie");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
|
||||
// 'rotate(36deg)', "right mask should be rotated from 360*(10/100) = 36 degrees");
|
||||
//
|
||||
// // switch to edit mode and check the result
|
||||
// testUtils.dom.click( target.querySelector('tbody td:not(.o_list_record_selector)'));
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_pie_value').textContent,
|
||||
// '10%', "should have 10% as pie value since int_field=10");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').attr('style'),
|
||||
// 'rotate(180deg)', "left mask should be covering the whole right side of the pie");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
|
||||
// 'rotate(36deg)', "right mask should be rotated from 360*(10/100) = 36 degrees");
|
||||
//
|
||||
// // save
|
||||
// testUtils.dom.click( list.$buttons.find('.o_list_button_save'));
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_pie_value').textContent,
|
||||
// '10%', "should have 10% as pie value since int_field=10");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').attr('style'),
|
||||
// 'rotate(180deg)', "left mask should be covering the whole right side of the pie");
|
||||
// assert.strictEqual(target.querySelector('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
|
||||
// 'rotate(36deg)', "right mask should be rotated from 360*(10/100) = 36 degrees");
|
||||
//
|
||||
// list.destroy();
|
||||
// });
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
clickSave,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit } from "@odoo/hoot-dom";
|
||||
|
||||
class Partner extends models.Model {
|
||||
float_field = fields.Float({
|
||||
string: "Float_field",
|
||||
digits: [0, 1],
|
||||
});
|
||||
_records = [{ float_field: 0.44444 }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PercentageField in form view", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
onRpc("web_save", ({ args }) => {
|
||||
expect(args[1].float_field).toBe(0.24);
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="float_field" widget="percentage"/></form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("44.4");
|
||||
expect(".o_field_widget[name=float_field] span").toHaveText("%", {
|
||||
message: "The input should be followed by a span containing the percentage symbol.",
|
||||
});
|
||||
|
||||
await click("[name='float_field'] input");
|
||||
await edit("24");
|
||||
expect("[name='float_field'] input").toHaveValue("24");
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("24");
|
||||
});
|
||||
|
||||
test("PercentageField in form view without rounding error", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `<form><field name="float_field" widget="percentage"/></form>`,
|
||||
});
|
||||
|
||||
await click("[name='float_field'] input");
|
||||
await edit("28");
|
||||
|
||||
expect("[name='float_field'] input").toHaveValue("28");
|
||||
});
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clickSave, editInput, getFixture, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
float_field: {
|
||||
string: "Float_field",
|
||||
type: "float",
|
||||
digits: [0, 1],
|
||||
},
|
||||
},
|
||||
records: [{ float_field: 0.44444 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("PercentageField");
|
||||
|
||||
QUnit.test("PercentageField in form view", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="percentage"/>
|
||||
</form>`,
|
||||
mockRPC(route, { args, method }) {
|
||||
if (method === "write") {
|
||||
assert.strictEqual(
|
||||
args[1].float_field,
|
||||
0.24,
|
||||
"the correct float value should be saved"
|
||||
);
|
||||
}
|
||||
},
|
||||
resId: 1,
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=float_field] input").value,
|
||||
"44.4",
|
||||
"The input should be rendered without the percentage symbol."
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name=float_field] span").textContent,
|
||||
"%",
|
||||
"The input should be followed by a span containing the percentage symbol."
|
||||
);
|
||||
const field = target.querySelector("[name='float_field'] input");
|
||||
await editInput(target, "[name='float_field'] input", "24");
|
||||
assert.strictEqual(field.value, "24", "The value should not be formated yet.");
|
||||
await clickSave(target);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget input").value,
|
||||
"24",
|
||||
"The new value should be formatted properly."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("percentage field with placeholder", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="percentage" placeholder="Placeholder"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_field_widget[name='float_field'] input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_widget[name='float_field'] input").placeholder,
|
||||
"Placeholder"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("PercentageField in form view without rounding error", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="float_field" widget="percentage"/>
|
||||
</form>`,
|
||||
});
|
||||
await editInput(target, "[name='float_field'] input", "28");
|
||||
assert.strictEqual(target.querySelector("[name='float_field'] input").value, "28");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
import {
|
||||
clickSave,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, edit, pointerDown, queryFirst, queryOne } from "@odoo/hoot-dom";
|
||||
import { getNextTabableElement } from "@web/core/utils/ui";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char({ default: "My little Foo Value", trim: true });
|
||||
name = fields.Char();
|
||||
|
||||
_records = [{ foo: "yop" }, { foo: "blip" }];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
test("PhoneField in form view on normal screens (readonly)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
readonly: true,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_field_phone a").toHaveCount(1);
|
||||
expect(".o_field_phone a").toHaveText("yop");
|
||||
expect(".o_field_phone a").toHaveAttribute("href", "tel:yop");
|
||||
});
|
||||
|
||||
test("PhoneField in form view on normal screens (edit)", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(`input[type="tel"]`).toHaveCount(1);
|
||||
expect(`input[type="tel"]`).toHaveValue("yop");
|
||||
expect(".o_field_phone a").toHaveCount(1);
|
||||
expect(".o_field_phone a").toHaveText("Call");
|
||||
expect(".o_field_phone a").toHaveAttribute("href", "tel:yop");
|
||||
|
||||
// change value in edit mode
|
||||
await click(`input[type="tel"]`);
|
||||
await edit("new");
|
||||
await animationFrame();
|
||||
// save
|
||||
await clickSave();
|
||||
expect(`input[type="tel"]`).toHaveValue("new");
|
||||
});
|
||||
|
||||
test("PhoneField in editable list view on normal screens", async () => {
|
||||
onRpc("has_group", () => true);
|
||||
await mountView({
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: '<list editable="bottom"><field name="foo" widget="phone"/></list>',
|
||||
});
|
||||
expect("tbody td:not(.o_list_record_selector).o_data_cell").toHaveCount(2);
|
||||
expect("tbody td:not(.o_list_record_selector) a:first").toHaveText("yop");
|
||||
expect(".o_field_widget a.o_form_uri").toHaveCount(2);
|
||||
|
||||
// Edit a line and check the result
|
||||
const cell = queryFirst("tbody td:not(.o_list_record_selector)");
|
||||
await click(cell);
|
||||
await animationFrame();
|
||||
expect(cell.parentElement).toHaveClass("o_selected_row");
|
||||
expect(`tbody td:not(.o_list_record_selector) input`).toHaveValue("yop");
|
||||
|
||||
await click(`tbody td:not(.o_list_record_selector) input`);
|
||||
await edit("new");
|
||||
await animationFrame();
|
||||
await click(".o_control_panel_main_buttons .o_list_button_save");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_selected_row").toHaveCount(0);
|
||||
expect("tbody td:not(.o_list_record_selector) a:first").toHaveText("new");
|
||||
expect(".o_field_widget a.o_form_uri").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("use TAB to navigate to a PhoneField", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await pointerDown(".o_field_widget[name=name] input");
|
||||
|
||||
expect(".o_field_widget[name=name] input").toBeFocused();
|
||||
expect(queryOne`[name="foo"] input:only`).toBe(getNextTabableElement());
|
||||
});
|
||||
|
||||
test("phone field with placeholder", async () => {
|
||||
Partner._fields.foo.default = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='foo'] input").toHaveProperty("placeholder", "Placeholder");
|
||||
});
|
||||
|
||||
test("placeholder_field shows as placeholder", async () => {
|
||||
Partner._fields.char = fields.Char({
|
||||
default: "My Placeholder",
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: `<form>
|
||||
<field name="foo" widget="phone" options="{'placeholder_field' : 'char'}"/>
|
||||
<field name="char"/>
|
||||
</form>`,
|
||||
});
|
||||
expect(`.o_field_phone input`).toHaveAttribute("placeholder", "My Placeholder");
|
||||
});
|
||||
|
||||
test("unset and readonly PhoneField", async () => {
|
||||
Partner._fields.foo.default = false;
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone" readonly="1" placeholder="Placeholder"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
expect(".o_field_widget[name='foo'] a").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("href is correctly formatted", async () => {
|
||||
Partner._records[0].foo = "+12 345 67 89 00";
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
readonly: true,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_field_phone a").toHaveText("+12 345 67 89 00");
|
||||
expect(".o_field_phone a").toHaveAttribute("href", "tel:+12345678900");
|
||||
});
|
||||
|
||||
test("New record, fill in phone field, then click on call icon and save", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" required="1"/>
|
||||
<field name="foo" widget="phone"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await contains(".o_field_widget[name=name] input").edit("TEST");
|
||||
await contains(".o_field_widget[name=foo] input").edit("+12345678900");
|
||||
|
||||
await click(`input[type="tel"]`);
|
||||
|
||||
expect(`.o_form_status_indicator_buttons`).not.toHaveClass("invisible");
|
||||
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_widget[name=name] input").toHaveValue("TEST");
|
||||
expect(".o_field_widget[name=foo] input").toHaveValue("+12345678900");
|
||||
expect(`.o_form_status_indicator_buttons`).toHaveClass("invisible");
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue