Initial commit: Core packages

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

View file

@ -0,0 +1,128 @@
/** @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"]]']);
}
);
});

View file

@ -0,0 +1,130 @@
/** @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');
});
});

View file

@ -0,0 +1,187 @@
/** @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"
);
}
);
});

View file

@ -0,0 +1,581 @@
/** @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,
""
);
})
});

View file

@ -0,0 +1,196 @@
/** @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"
);
});
});

View file

@ -0,0 +1,276 @@
/** @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"
);
});
});

View file

@ -0,0 +1,280 @@
/** @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([]);
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
/** @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"
);
});
});

View file

@ -0,0 +1,243 @@
/** @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)"
);
});
});

View file

@ -0,0 +1,270 @@
/** @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",
]);
});
});

View file

@ -0,0 +1,774 @@
/** @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

View file

@ -0,0 +1,661 @@
/** @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

View file

@ -0,0 +1,259 @@
/** @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"
);
});
});

View file

@ -0,0 +1,63 @@
/** @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."
);
});
});

View file

@ -0,0 +1,479 @@
/** @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"
);
});
});

View file

@ -0,0 +1,194 @@
/** @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);
});
});

View file

@ -0,0 +1,117 @@
/** @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"
);
});
});

View file

@ -0,0 +1,91 @@
/** @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);
}
);
});

View file

@ -0,0 +1,358 @@
/** @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");
});
});

View file

@ -0,0 +1,153 @@
/** @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"
);
});
});

View file

@ -0,0 +1,298 @@
/** @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
});
});

View file

@ -0,0 +1,86 @@
/** @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>");
});
});

View file

@ -0,0 +1,963 @@
/** @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"));
}
);
});

View file

@ -0,0 +1,282 @@
/** @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"
);
});
});

View file

@ -0,0 +1,386 @@
/** @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");
});
});

View file

@ -0,0 +1,185 @@
/** @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"
);
}
);
});

View file

@ -0,0 +1,221 @@
/** @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'"
);
});
});

View file

@ -0,0 +1,293 @@
/** @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");
});
});

View file

@ -0,0 +1,411 @@
/** @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

View file

@ -0,0 +1,456 @@
/** @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"
);
});
});

View file

@ -0,0 +1,342 @@
/** @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");
});
});

View file

@ -0,0 +1,210 @@
/** @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

View file

@ -0,0 +1,69 @@
/** @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, "");
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,361 @@
/** @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

View file

@ -0,0 +1,208 @@
/** @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);
});
});

View file

@ -0,0 +1,104 @@
/** @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");
});
});

View file

@ -0,0 +1,302 @@
/** @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();
// });
});

View file

@ -0,0 +1,108 @@
/** @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");
});
});

View file

@ -0,0 +1,263 @@
/** @odoo-module **/
import { getNextTabableElement } from "@web/core/utils/ui";
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,
},
},
records: [{ foo: "yop" }, { foo: "blip" }],
},
},
};
setupViewRegistries();
});
QUnit.module("PhoneField");
QUnit.test("PhoneField in form view on normal screens (readonly)", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
mode: "readonly",
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="phone"/>
</group>
</sheet>
</form>`,
resId: 1,
});
const phone = target.querySelector(".o_field_phone a");
assert.containsOnce(
target,
phone,
"should have rendered the phone number as a link with correct classes"
);
assert.strictEqual(phone.textContent, "yop", "value should be displayed properly");
assert.hasAttrValue(phone, "href", "tel:yop", "should have proper tel prefix");
});
QUnit.test("PhoneField in form view on normal screens (edit)", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="phone"/>
</group>
</sheet>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
'input[type="tel"]',
"should have an input for the phone field"
);
assert.strictEqual(
target.querySelector('input[type="tel"]').value,
"yop",
"input should contain field value in edit mode"
);
const phoneLink = target.querySelector(".o_field_phone a");
assert.containsOnce(
target,
phoneLink,
"should have rendered the phone number as a link with correct classes"
);
assert.strictEqual(phoneLink.textContent, "Call", "link is shown with the right text");
assert.hasAttrValue(phoneLink, "href", "tel:yop", "should have proper tel prefix");
// change value in edit mode
await editInput(target, "input[type='tel']", "new");
// save
await clickSave(target);
assert.strictEqual(
target.querySelector("input[type='tel']").value,
"new",
"new value should be displayed properly"
);
});
QUnit.test("PhoneField in editable list view on normal screens", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "partner",
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
});
assert.containsN(target, "tbody td:not(.o_list_record_selector).o_data_cell", 2);
assert.strictEqual(
target.querySelector("tbody td:not(.o_list_record_selector) a").textContent,
"yop",
"value should be displayed properly with a link to send SMS"
);
assert.containsN(
target,
".o_field_widget a.o_form_uri",
2,
"should have the correct classnames"
);
// 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");
assert.strictEqual(
cell.querySelector("input").value,
"yop",
"should have the corect value in internal input"
);
await editInput(cell, "input", "new");
// save
await click(target.querySelector(".o_list_button_save"));
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) a").textContent,
"new",
"value should be properly updated"
);
assert.containsN(
target,
".o_field_widget a.o_form_uri",
2,
"should still have links with correct classes"
);
});
QUnit.test("use TAB to navigate to a PhoneField", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="display_name"/>
<field name="foo" widget="phone"/>
</group>
</sheet>
</form>`,
});
target.querySelector(".o_field_widget[name=display_name] input").focus();
assert.strictEqual(
document.activeElement,
target.querySelector('.o_field_widget[name="display_name"] input'),
"display_name should be focused"
);
assert.strictEqual(
getNextTabableElement(target),
target.querySelector('[name="foo"] input'),
"foo should be focused"
);
});
QUnit.test("phone 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" widget="phone" placeholder="Placeholder"/>
</group>
</sheet>
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").placeholder,
"Placeholder"
);
});
QUnit.test("unset and readonly PhoneField", async function (assert) {
serverData.models.partner.fields.foo.default = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="phone" readonly="1" placeholder="Placeholder"/>
</group>
</sheet>
</form>`,
});
assert.containsNone(
target.querySelector(".o_field_widget[name='foo']"),
"a",
"The readonly field don't contain a link if no value is set"
);
});
QUnit.test("href is correctly formatted", async function (assert) {
serverData.models.partner.records[0].foo = "+12 345 67 89 00";
await makeView({
serverData,
type: "form",
resModel: "partner",
mode: "readonly",
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="phone"/>
</group>
</sheet>
</form>`,
resId: 1,
});
const phone = target.querySelector(".o_field_phone a");
assert.strictEqual(
phone.textContent,
"+12 345 67 89 00",
"value should be displayed properly with spaces as separators"
);
assert.hasAttrValue(phone, "href", "tel:+12345678900", "href should not contain any space");
});
});

View file

@ -0,0 +1,671 @@
/** @odoo-module **/
import {
click,
clickSave,
getFixture,
nextTick,
triggerEvent,
triggerHotkey,
} 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",
},
sequence: { type: "integer", string: "Sequence", searchable: true },
selection: {
string: "Selection",
type: "selection",
selection: [
["normal", "Normal"],
["blocked", "Blocked"],
["done", "Done"],
],
},
},
records: [
{
id: 1,
foo: "yop",
selection: "blocked",
},
{
id: 2,
foo: "blip",
selection: "normal",
},
{
id: 4,
foo: "abc",
selection: "done",
},
{ id: 3, foo: "gnap" },
{ id: 5, foo: "blop" },
],
},
},
};
setupViewRegistries();
});
QUnit.module("PriorityField");
QUnit.test("PriorityField when not set", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 2,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="priority" />
</group>
</sheet>
</form>`,
});
assert.containsOnce(
target,
".o_field_widget .o_priority:not(.o_field_empty)",
"widget should be considered set, even though there is no value for this field"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsNone(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
"should have no full star since there is no value"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
2,
"should have two empty stars since there is no value"
);
});
QUnit.test("PriorityField tooltip", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="priority"/>
</group>
</sheet>
</form>`,
resId: 1,
});
// check data-tooltip attribute (used by the tooltip service)
const stars = target.querySelectorAll(".o_field_widget .o_priority a.o_priority_star");
assert.strictEqual(
stars[0].dataset["tooltip"],
"Selection: Blocked",
"Should set field label and correct selection label as title attribute (tooltip)"
);
assert.strictEqual(
stars[1].dataset["tooltip"],
"Selection: Done",
"Should set field label and correct selection label as title attribute (tooltip)"
);
});
QUnit.test("PriorityField in form view", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="priority" />
</group>
</sheet>
</form>`,
});
assert.containsOnce(
target,
".o_field_widget .o_priority:not(.o_field_empty)",
"widget should be considered set"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
"should have one full star since the value is the second value"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should have one empty star since the value is the second value"
);
// hover last star
let stars = target.querySelectorAll(
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
);
await triggerEvent(stars[stars.length - 1], null, "mouseenter");
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
2,
"should temporary have two full stars since we are hovering the third value"
);
assert.containsNone(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should temporary have no empty star since we are hovering the third value"
);
await triggerEvent(stars[stars.length - 1], null, "mouseleave");
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
"should temporary have two full stars since we are hovering the third value"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should temporary have no empty star since we are hovering the third value"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
"should still have one full star since the value is the second value"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should still have one empty star since the value is the second value"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
"should still have one full star since the value is the second value"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should still have one empty star since the value is the second value"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
"should still have one full star since the value is the second value"
);
assert.containsOnce(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should still have one empty star since the value is the second value"
);
// click on the second star in edit mode
stars = target.querySelectorAll(".o_field_widget .o_priority a.o_priority_star.fa-star-o");
await click(stars[stars.length - 1]);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
2,
"should now have two full stars since the value is the third value"
);
assert.containsNone(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should now have no empty star since the value is the third value"
);
// save
await clickSave(target);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsN(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
2,
"should now have two full stars since the value is the third value"
);
assert.containsNone(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star-o",
"should now have no empty star since the value is the third value"
);
});
QUnit.test("PriorityField can write after adding a record -- kanban", async function (assert) {
serverData.models.partner.fields.selection.selection = [
["0", 0],
["1", 1],
];
serverData.models.partner.records[0].selection = "0";
serverData.views = {
"partner,myquickview,form": `<form><field name="display_name" /></form>`,
};
await makeView({
type: "kanban",
resModel: "partner",
serverData,
domain: [["id", "=", 1]],
groupBy: ["foo"],
arch: `
<kanban on_create="quick_create" quick_create_view="myquickview">
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click">
<field name="selection" widget="priority"/>
</div>
</t>
</templates>
</kanban>`,
mockRPC(route, args) {
if (args.method === "write") {
assert.step(`write ${JSON.stringify(args.args)}`);
}
},
});
assert.containsNone(target, ".o_kanban_record .fa-star");
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"), null, true);
assert.verifySteps(['write [[1],{"selection":"1"}]']);
assert.containsOnce(target, ".o_kanban_record .fa-star");
await click(target, ".o-kanban-button-new");
await click(target, ".o_kanban_quick_create .o_kanban_add");
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"), null, true);
assert.verifySteps(['write [[6],{"selection":"1"}]']);
assert.containsN(target, ".o_kanban_record .fa-star", 2);
});
QUnit.test("PriorityField in editable list view", async function (assert) {
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree editable="bottom">
<field name="selection" widget="priority" />
</tree>`,
});
assert.containsOnce(
target.querySelectorAll(".o_data_row")[0],
".o_priority:not(.o_field_empty)",
"widget should be considered set"
);
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsOnce(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star",
"should have one full star since the value is the second value"
);
assert.containsOnce(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star-o",
"should have one empty star since the value is the second value"
);
// switch to edit mode and check the result
await click(target.querySelector("tbody td:not(.o_list_record_selector)"));
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsOnce(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star",
"should have one full star since the value is the second value"
);
assert.containsOnce(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star-o",
"should have one empty star since the value is the second value"
);
// save
await click(target, ".o_list_button_save");
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsOnce(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star",
"should have one full star since the value is the second value"
);
assert.containsOnce(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star-o",
"should have one empty star since the value is the second value"
);
// hover last star
await triggerEvent(
target.querySelector(".o_data_row"),
".o_priority a.o_priority_star.fa-star-o",
"mouseenter"
);
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star",
2,
"should have two stars for representing each possible value: no star, one star and two stars"
);
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
"a.o_priority_star.fa-star",
2,
"should temporary have two full stars since we are hovering the third value"
);
assert.containsNone(
target.querySelectorAll(".o_data_row")[0],
"a.o_priority_star.fa-star-o",
"should temporary have no empty star since we are hovering the third value"
);
// click on the first star in readonly mode
await click(target.querySelector(".o_priority a.o_priority_star.fa-star"));
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsNone(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star",
"should now have no full star since the value is the first value"
);
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star-o",
2,
"should now have two empty stars since the value is the first value"
);
// re-enter edit mode to force re-rendering the widget to check if the value was correctly saved
await click(target.querySelector("tbody td:not(.o_list_record_selector)"));
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsNone(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star",
"should now only have no full star since the value is the first value"
);
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
".o_priority a.o_priority_star.fa-star-o",
2,
"should now have two empty stars since the value is the first value"
);
// Click on second star in edit mode
const stars = target.querySelectorAll(".o_priority a.o_priority_star.fa-star-o");
await click(stars[stars.length - 1]);
let rows = target.querySelectorAll(".o_data_row");
assert.containsN(
rows[rows.length - 1],
".o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsN(
rows[rows.length - 1],
".o_priority a.o_priority_star.fa-star",
2,
"should now have two full stars since the value is the third value"
);
assert.containsNone(
rows[rows.length - 1],
".o_priority a.o_priority_star.fa-star-o",
"should now have no empty star since the value is the third value"
);
// save
await click(target, ".o_list_button_save");
rows = target.querySelectorAll(".o_data_row");
assert.containsN(
rows[rows.length - 1],
".o_priority a.o_priority_star",
2,
"should still have two stars"
);
assert.containsN(
rows[rows.length - 1],
".o_priority a.o_priority_star.fa-star",
2,
"should now have two full stars since the value is the third value"
);
assert.containsNone(
rows[rows.length - 1],
".o_priority a.o_priority_star.fa-star-o",
"should now have no empty star since the value is the third value"
);
});
QUnit.test("PriorityField with readonly attribute", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 2,
serverData,
arch: '<form><field name="selection" widget="priority" readonly="1"/></form>',
mockRPC(route, args) {
if (args.method === "write") {
throw new Error("should not save");
}
},
});
assert.containsN(
target,
"span.o_priority_star.fa.fa-star-o",
2,
"stars of priority widget should rendered with span tag if readonly"
);
await triggerEvent(
target.querySelectorAll(".o_priority_star.fa-star-o")[1],
null,
"mouseenter"
);
assert.containsNone(
target,
".o_field_widget .o_priority a.o_priority_star.fa-star",
"should have no full stars on hover since the field is readonly"
);
await click(target.querySelectorAll(".o_priority_star.fa-star-o")[1]);
assert.containsN(
target,
"span.o_priority_star.fa.fa-star-o",
2,
"should still have two stars"
);
});
QUnit.test(
'PriorityField edited by the smart action "Set priority..."',
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="selection" widget="priority"/>
</form>`,
resId: 1,
});
assert.containsOnce(target, "a.fa-star");
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")]
.map((el) => el.textContent)
.indexOf("Set priority...ALT + R");
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx]);
await nextTick();
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Normal", "Blocked", "Done"]
);
await click(target, "#o_command_2");
await nextTick();
assert.containsN(target, "a.fa-star", 2);
}
);
QUnit.test("PriorityField - auto save record when field toggled", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="priority" />
</group>
</sheet>
</form>`,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
}
},
});
const stars = target.querySelectorAll(
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
);
await click(stars[stars.length - 1]);
assert.verifySteps(["write"]);
});
QUnit.test("PriorityField - prevent auto save with autosave option", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="priority" options="{'autosave': False}"/>
</group>
</sheet>
</form>`,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
}
},
});
const stars = target.querySelectorAll(
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
);
await click(stars[stars.length - 1]);
assert.verifySteps([]);
});
});

View file

@ -0,0 +1,541 @@
/** @odoo-module **/
import {
makeFakeLocalizationService,
makeFakeNotificationService,
} from "@web/../tests/helpers/mock_services";
import {
click,
clickSave,
editInput,
getFixture,
nextTick,
patchWithCleanup,
} 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",
},
int_field2: {
string: "int_field",
type: "integer",
},
int_field3: {
string: "int_field",
type: "integer",
},
float_field: {
string: "Float_field",
type: "float",
digits: [16, 1],
},
},
records: [
{
int_field: 10,
float_field: 0.44444,
},
],
},
},
};
setupViewRegistries();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
});
QUnit.module("ProgressBarField");
QUnit.test("ProgressBarField: max_value should update", async function (assert) {
assert.expect(3);
serverData.models.partner.records[0].float_field = 2;
serverData.models.partner.onchanges = {
display_name(obj) {
obj.int_field = 999;
obj.float_field = 5;
},
};
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="display_name" />
<field name="float_field" invisible="1" />
<field name="int_field" widget="progressbar" options="{'current_value': 'int_field', 'max_value': 'float_field'}" />
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.deepEqual(
args[1],
{ int_field: 999, float_field: 5, display_name: "new name" },
"New value of progress bar saved"
);
}
},
});
assert.strictEqual(
target.querySelector(".o_progressbar").textContent,
"10 / 2",
"The initial value of the progress bar should be correct"
);
await editInput(target, ".o_field_widget[name=display_name] input", "new name");
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_progressbar").textContent,
"999 / 5",
"The value of the progress bar should be correct after the update"
);
});
QUnit.test(
"ProgressBarField: value should update in edit mode when typing in input",
async function (assert) {
assert.expect(4);
serverData.models.partner.records[0].int_field = 99;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.strictEqual(
args[1].int_field,
69,
"New value of progress bar saved"
);
}
},
});
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value +
target.querySelector(".o_progressbar").textContent,
"99%",
"Initial value should be correct"
);
await editInput(target, ".o_progressbar_value .o_input", "69");
await click(target, ".o_form_view");
await nextTick();
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value,
"69",
"New value should be different after focusing out of the field"
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value,
"69",
"New value is still displayed after save"
);
}
);
QUnit.test(
"ProgressBarField: value should update in edit mode when typing in input with field max value",
async function (assert) {
assert.expect(4);
serverData.models.partner.records[0].int_field = 99;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="float_field" invisible="1" />
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.strictEqual(
args[1].int_field,
69,
"New value of progress bar saved"
);
}
},
});
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value +
target.querySelector(".o_progressbar").textContent,
"99 / 0",
"Initial value should be correct"
);
await editInput(target, ".o_progressbar_value .o_input", "69");
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value +
target.querySelector(".o_progressbar").textContent,
"69 / 0",
"New value should be different than initial after click"
);
}
);
QUnit.test(
"ProgressBarField: max value should update in edit mode when typing in input with field max value",
async function (assert) {
assert.expect(5);
serverData.models.partner.records[0].int_field = 99;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="float_field" invisible="1" />
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field', 'edit_max_value': true}" />
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.strictEqual(
args[1].float_field,
69,
"New value of progress bar saved"
);
}
},
});
assert.strictEqual(
target.querySelector(".o_progressbar").textContent +
target.querySelector(".o_progressbar_value .o_input").value,
"99 / 0",
"Initial value should be correct"
);
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
target.querySelector(".o_progressbar input").focus();
await nextTick();
assert.strictEqual(
target.querySelector(".o_progressbar").textContent +
target.querySelector(".o_progressbar_value .o_input").value,
"99 / 0.44",
"Initial value is not formatted when focused"
);
await editInput(target, ".o_progressbar_value .o_input", "69");
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_progressbar").textContent +
target.querySelector(".o_progressbar_value .o_input").value,
"99 / 69",
"New value should be different than initial after click"
);
}
);
QUnit.test("ProgressBarField: Standard readonly mode is readonly", async function (assert) {
serverData.models.partner.records[0].int_field = 99;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form edit="0">
<field name="float_field" invisible="1"/>
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field', 'edit_max_value': true}"/>
</form>`,
resId: 1,
mockRPC(route) {
assert.step(route);
},
});
assert.strictEqual(
target.querySelector(".o_progressbar").textContent,
"99 / 0",
"Initial value should be correct"
);
await click(target.querySelector(".o_progress"));
assert.containsNone(target, ".o_progressbar_value .o_input", "no input in readonly mode");
assert.verifySteps([
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
]);
});
QUnit.test("ProgressBarField: field is editable in kanban", async function (assert) {
assert.expect(7);
serverData.models.partner.fields.int_field.readonly = true;
serverData.models.partner.records[0].int_field = 99;
await makeView({
serverData,
type: "kanban",
resModel: "partner",
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" title="ProgressBarTitle" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
</div>
</t>
</templates>
</kanban>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.strictEqual(args[1].int_field, 69, "New value of progress bar saved");
}
},
});
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value,
"99",
"Initial input value should be correct"
);
assert.strictEqual(
target.querySelector(".o_progressbar_value span").textContent,
"100",
"Initial max value should be correct"
);
assert.strictEqual(
target.querySelector(".o_progressbar_title").textContent,
"ProgressBarTitle"
);
await editInput(target, ".o_progressbar_value .o_input", "69");
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value,
"69",
"New input value should now be different"
);
assert.strictEqual(
target.querySelector(".o_progressbar_value span").textContent,
"100",
"Max value is still the same be correct"
);
assert.strictEqual(
target.querySelector(".o_progressbar_title").textContent,
"ProgressBarTitle"
);
});
QUnit.test("force readonly in kanban", async (assert) => {
assert.expect(2);
serverData.models.partner.records[0].int_field = 99;
await makeView({
serverData,
type: "kanban",
resModel: "partner",
arch: /* xml */ `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" widget="progressbar" options="{'editable': true, 'max_value': 'float_field', 'readonly': True}" />
</div>
</t>
</templates>
</kanban>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
throw new Error("Not supposed to write");
}
},
});
assert.strictEqual(target.querySelector(".o_progressbar").textContent, "99 / 100");
assert.containsNone(target, ".o_progressbar_value .o_input");
});
QUnit.test(
"ProgressBarField: readonly and editable attrs/options in kanban",
async function (assert) {
assert.expect(4);
serverData.models.partner.records[0].int_field = 29;
serverData.models.partner.records[0].int_field2 = 59;
serverData.models.partner.records[0].int_field3 = 99;
await makeView({
serverData,
type: "kanban",
resModel: "partner",
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" readonly="1" widget="progressbar" options="{'max_value': 'float_field'}" />
<field name="int_field2" widget="progressbar" options="{'max_value': 'float_field'}" />
<field name="int_field3" widget="progressbar" options="{'editable': true, 'max_value': 'float_field'}" />
</div>
</t>
</templates>
</kanban>`,
resId: 1,
});
assert.containsNone(
target,
"[name='int_field'] .o_progressbar_value .o_input",
"the field is still in readonly since there is readonly attribute"
);
assert.containsNone(
target,
"[name='int_field2'] .o_progressbar_value .o_input",
"the field is still in readonly since the editable option is missing"
);
assert.containsOnce(
target,
"[name='int_field3'] .o_progressbar_value .o_input",
"the field is still in readonly since the editable option is missing"
);
await editInput(
target,
".o_field_progressbar[name='int_field3'] .o_progressbar_value .o_input",
"69"
);
assert.strictEqual(
target.querySelector(
".o_field_progressbar[name='int_field3'] .o_progressbar_value .o_input"
).value,
"69",
"New value should be different than initial after click"
);
}
);
QUnit.test(
"ProgressBarField: write float instead of int works, in locale",
async function (assert) {
assert.expect(4);
serverData.models.partner.records[0].int_field = 99;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
</form>`,
resId: 1,
mockRPC: function (route, { method, args }) {
if (method === "write") {
assert.strictEqual(
args[1].int_field,
1037,
"New value of progress bar saved"
);
}
},
});
registry.category("services").remove("localization");
registry
.category("services")
.add(
"localization",
makeFakeLocalizationService({ thousandsSep: "#", decimalPoint: ":" })
);
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value +
target.querySelector(".o_progressbar").textContent,
"99%",
"Initial value should be correct"
);
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
await editInput(target, ".o_field_widget input", "1#037:9");
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value,
"1k",
"New value should be different than initial after click"
);
}
);
QUnit.test(
"ProgressBarField: write gibbrish instead of int throws warning",
async function (assert) {
serverData.models.partner.records[0].int_field = 99;
const mock = () => {
assert.step("Show error message");
return () => {};
};
registry.category("services").add("notification", makeFakeNotificationService(mock), {
force: true,
});
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="int_field" widget="progressbar" options="{'editable': true}"/>
</form>`,
resId: 1,
});
assert.strictEqual(
target.querySelector(".o_progressbar_value .o_input").value,
"99",
"Initial value in input is correct"
);
await editInput(target, ".o_progressbar_value .o_input", "trente sept virgule neuf");
await clickSave(target);
assert.containsOnce(target, ".o_form_dirty", "The form has not been saved");
assert.verifySteps(["Show error message"], "The error message was shown correctly");
}
);
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,363 @@
/** @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(() => {
serverData = {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
bar: { string: "Bar", type: "boolean", default: true },
int_field: { string: "int_field", type: "integer", sortable: true },
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
product_id: { string: "Product", type: "many2one", relation: "product" },
color: {
type: "selection",
selection: [
["red", "Red"],
["black", "Black"],
],
default: "red",
string: "Color",
},
},
records: [
{
id: 1,
display_name: "first record",
bar: true,
int_field: 10,
trululu: 4,
},
{
id: 2,
display_name: "second record",
},
{
id: 3,
display_name: "third record",
},
],
onchanges: {},
},
product: {
fields: {
name: { string: "Product Name", type: "char" },
},
records: [
{
id: 37,
display_name: "xphone",
},
{
id: 41,
display_name: "xpad",
},
],
},
},
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("RadioField");
QUnit.test("fieldradio widget on a many2one in a new record", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="product_id" widget="radio"/></form>',
});
assert.ok(
target.querySelectorAll("div.o_radio_item").length,
"should have rendered outer div"
);
assert.containsN(target, "input.o_radio_input", 2, "should have 2 possible choices");
assert.strictEqual(
target.querySelector(".o_field_radio").textContent.replace(/\s+/g, ""),
"xphonexpad"
);
assert.containsNone(target, "input:checked", "none of the input should be checked");
await click(target.querySelectorAll("input.o_radio_input")[0]);
assert.containsOnce(target, "input:checked", "one of the input should be checked");
await clickSave(target);
assert.hasAttrValue(
target.querySelector("input.o_radio_input:checked"),
"data-value",
"37",
"should have saved record with correct value"
);
});
QUnit.test("required fieldradio widget on a many2one", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: '<form><field name="product_id" widget="radio" required="1"/></form>',
});
assert.containsNone(
target,
".o_field_radio input:checked",
"none of the input should be checked"
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_notification_title").textContent,
"Invalid fields: "
);
assert.strictEqual(
target.querySelector(".o_notification_content").innerHTML,
"<ul><li>Product</li></ul>"
);
assert.hasClass(target.querySelector(".o_notification"), "border-danger");
});
QUnit.test("fieldradio change value by onchange", async function (assert) {
serverData.models.partner.onchanges = {
bar(obj) {
obj.product_id = obj.bar ? [41] : [37];
obj.color = obj.bar ? "red" : "black";
},
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="bar" />
<field name="product_id" widget="radio" />
<field name="color" widget="radio" />
</form>`,
});
await click(target, "input[type='checkbox']");
assert.containsOnce(
target,
"input.o_radio_input[data-value='37']:checked",
"one of the input should be checked"
);
assert.containsOnce(
target,
"input.o_radio_input[data-value='black']:checked",
"the other of the input should be checked"
);
await click(target, "input[type='checkbox']");
assert.containsOnce(
target,
"input.o_radio_input[data-value='41']:checked",
"the other of the input should be checked"
);
assert.containsOnce(
target,
"input.o_radio_input[data-value='red']:checked",
"one of the input should be checked"
);
});
QUnit.test("fieldradio widget on a selection in a new record", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="color" widget="radio"/></form>',
});
assert.ok(
target.querySelectorAll("div.o_radio_item").length,
"should have rendered outer div"
);
assert.containsN(target, "input.o_radio_input", 2, "should have 2 possible choices");
assert.strictEqual(
target.querySelector(".o_field_radio").textContent.replace(/\s+/g, ""),
"RedBlack"
);
// click on 2nd option
await click(target.querySelectorAll("input.o_radio_input")[1]);
await clickSave(target);
assert.hasAttrValue(
target.querySelector("input.o_radio_input:checked"),
"data-value",
"black",
"should have saved record with correct value"
);
});
QUnit.test("fieldradio widget has o_horizontal or o_vertical class", async function (assert) {
serverData.models.partner.fields.color2 = serverData.models.partner.fields.color;
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<group>
<field name="color" widget="radio" />
<field name="color2" widget="radio" options="{'horizontal': True}" />
</group>
</form>`,
});
assert.containsOnce(
target,
".o_field_radio > div.o_vertical",
"should have o_vertical class"
);
const verticalRadio = target.querySelector(".o_field_radio > div.o_vertical");
assert.strictEqual(
verticalRadio.querySelector(".o_radio_item:first-child").getBoundingClientRect().right,
verticalRadio.querySelector(".o_radio_item:last-child").getBoundingClientRect().right
);
assert.containsOnce(
target,
".o_field_radio > div.o_horizontal",
"should have o_horizontal class"
);
const horizontalRadio = target.querySelector(".o_field_radio > div.o_horizontal");
assert.strictEqual(
horizontalRadio.querySelector(".o_radio_item:first-child").getBoundingClientRect().top,
horizontalRadio.querySelector(".o_radio_item:last-child").getBoundingClientRect().top
);
});
QUnit.test("fieldradio widget with numerical keys encoded as strings", async function (assert) {
assert.expect(5);
serverData.models.partner.fields.selection = {
type: "selection",
selection: [
["0", "Red"],
["1", "Black"],
],
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="selection" widget="radio"/></form>',
mockRPC: function (route, { args, method, model }) {
if (model === "partner" && method === "write") {
assert.strictEqual(args[1].selection, "1", "should write correct value");
}
},
});
assert.strictEqual(
target.querySelector(".o_field_widget").textContent.replace(/\s+/g, ""),
"RedBlack"
);
assert.containsNone(target, ".o_radio_input:checked", "no value should be checked");
await click(target.querySelectorAll("input.o_radio_input")[1]);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_widget").textContent.replace(/\s+/g, ""),
"RedBlack"
);
assert.containsOnce(
target,
".o_radio_input[data-index='1']:checked",
"'Black' should be checked"
);
});
QUnit.test(
"widget radio on a many2one: domain updated by an onchange",
async function (assert) {
assert.expect(4);
serverData.models.partner.onchanges = {
int_field() {},
};
let domain = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="int_field" />
<field name="trululu" widget="radio" />
</form>`,
mockRPC(route, { kwargs, method }) {
if (method === "onchange") {
domain = [["id", "in", [10]]];
return Promise.resolve({
value: {
trululu: false,
},
domain: {
trululu: domain,
},
});
}
if (method === "search_read") {
assert.deepEqual(kwargs.domain, domain, "sent domain should be correct");
}
},
});
assert.containsN(
target,
".o_field_widget[name='trululu'] .o_radio_item",
3,
"should be 3 radio buttons"
);
// trigger an onchange that will update the domain
await editInput(target, ".o_field_widget[name='int_field'] input", "2");
assert.containsNone(
target,
".o_field_widget[name='trululu'] .o_radio_item",
"should be no more radio button"
);
}
);
QUnit.test("field is empty", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 2,
serverData,
arch: `
<form edit="0">
<field name="trululu" widget="radio" />
</form>`,
});
assert.hasClass(target.querySelector(".o_field_widget[name=trululu]"), "o_field_empty");
assert.containsN(target, ".o_radio_input", 3);
assert.containsN(target, ".o_radio_input:disabled", 3);
assert.containsNone(target, ".o_radio_input:checked");
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,509 @@
/** @odoo-module **/
import {
click,
editInput,
getFixture,
patchDate,
patchTimeZone,
} 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 },
},
records: [],
onchanges: {},
},
},
};
setupViewRegistries();
});
QUnit.module("RemainingDaysField");
QUnit.test("RemainingDaysField on a date field in list view", async function (assert) {
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, date: "2017-10-08" }, // today
{ id: 2, date: "2017-10-09" }, // tomorrow
{ id: 3, date: "2017-10-07" }, // yesterday
{ id: 4, date: "2017-10-10" }, // + 2 days
{ id: 5, date: "2017-10-05" }, // - 3 days
{ id: 6, date: "2018-02-08" }, // + 4 months (diff >= 100 days)
{ id: 7, date: "2017-06-08" }, // - 4 months (diff >= 100 days)
{ id: 8, date: false },
];
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="date" widget="remaining_days" />
</tree>`,
});
const cells = target.querySelectorAll(".o_data_cell");
assert.strictEqual(cells[0].textContent, "Today");
assert.strictEqual(cells[1].textContent, "Tomorrow");
assert.strictEqual(cells[2].textContent, "Yesterday");
assert.strictEqual(cells[3].textContent, "In 2 days");
assert.strictEqual(cells[4].textContent, "3 days ago");
assert.strictEqual(cells[5].textContent, "02/08/2018");
assert.strictEqual(cells[6].textContent, "06/08/2017");
assert.strictEqual(cells[7].textContent, "");
assert.hasAttrValue(cells[0].querySelector(".o_field_widget > div"), "title", "10/08/2017");
assert.hasClass(cells[0].querySelector(".o_field_widget > div"), "fw-bold text-warning");
assert.doesNotHaveClass(
cells[1].querySelector(".o_field_widget > div"),
"fw-bold text-warning text-danger"
);
assert.hasClass(cells[2].querySelector(".o_field_widget > div"), "fw-bold text-danger");
assert.doesNotHaveClass(
cells[3].querySelector(".o_field_widget > div"),
"fw-bold text-warning text-danger"
);
assert.hasClass(cells[4].querySelector(".o_field_widget > div"), "fw-bold text-danger");
assert.doesNotHaveClass(
cells[5].querySelector(".o_field_widget > div"),
"fw-bold text-warning text-danger"
);
assert.hasClass(cells[6].querySelector(".o_field_widget > div"), "fw-bold text-danger");
});
QUnit.test(
"RemainingDaysField on a date field in multi edit list view",
async function (assert) {
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, date: "2017-10-08" }, // today
{ id: 2, date: "2017-10-09" }, // tomorrow
{ id: 8, date: false },
];
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree multi_edit="1">
<field name="date" widget="remaining_days" />
</tree>`,
});
const cells = target.querySelectorAll(".o_data_cell");
const rows = target.querySelectorAll(".o_data_row");
assert.strictEqual(cells[0].textContent, "Today");
assert.strictEqual(cells[1].textContent, "Tomorrow");
// 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",
"should have date picker input"
);
await editInput(target, ".o_datepicker_input", "10/10/2017");
await click(target);
assert.containsOnce(document.body, ".modal");
assert.strictEqual(
document.querySelector(".modal .o_field_widget").textContent,
"In 2 days",
"should have 'In 2 days' value to change"
);
await click(document.body, ".modal .modal-footer .btn-primary");
assert.strictEqual(
rows[0].querySelector(".o_data_cell").textContent,
"In 2 days",
"should have 'In 2 days' as date field value"
);
assert.strictEqual(
rows[1].querySelector(".o_data_cell").textContent,
"In 2 days",
"should have 'In 2 days' as date field value"
);
}
);
QUnit.test(
"RemainingDaysField, enter wrong value manually in multi edit list view",
async function (assert) {
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, date: "2017-10-08" }, // today
{ id: 2, date: "2017-10-09" }, // tomorrow
{ id: 8, date: false },
];
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree multi_edit="1">
<field name="date" widget="remaining_days" />
</tree>`,
});
const cells = target.querySelectorAll(".o_data_cell");
const rows = target.querySelectorAll(".o_data_row");
assert.strictEqual(cells[0].textContent, "Today");
assert.strictEqual(cells[1].textContent, "Tomorrow");
// 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",
"should have date picker input"
);
await editInput(target, ".o_datepicker_input", "blabla");
await click(target);
assert.containsNone(document.body, ".modal");
assert.strictEqual(cells[0].textContent, "Today");
assert.strictEqual(cells[1].textContent, "Tomorrow");
}
);
QUnit.test("RemainingDaysField on a date field in form view", async function (assert) {
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, date: "2017-10-08" }, // today
];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="date" widget="remaining_days" />
</form>`,
});
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/08/2017");
assert.containsOnce(target, ".o_form_editable");
assert.containsOnce(target, "div.o_field_widget[name='date'] .o_datepicker");
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
await click(target, ".o_form_button_save");
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/09/2017");
});
QUnit.test(
"RemainingDaysField on a date field on a new record in form",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="date" widget="remaining_days" />
</form>`,
});
assert.containsOnce(
target,
".o_form_editable .o_field_widget[name='date'] .o_datepicker"
);
await click(target.querySelector(".o_field_widget[name='date'] .o_datepicker input"));
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
}
);
QUnit.test("RemainingDaysField in form view (readonly)", async function (assert) {
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, date: "2017-10-08", datetime: "2017-10-08 10:00:00" }, // today
];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="date" widget="remaining_days" readonly="1" />
<field name="datetime" widget="remaining_days" readonly="1" />
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget[name='date']").textContent,
"Today"
);
assert.hasClass(
target.querySelector(".o_field_widget[name='date'] > div "),
"fw-bold text-warning"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name='datetime']").textContent,
"Today"
);
assert.hasClass(
target.querySelector(".o_field_widget[name='datetime'] > div "),
"fw-bold text-warning"
);
});
QUnit.test("RemainingDaysField on a datetime field in form view", async function (assert) {
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, datetime: "2017-10-08 10:00:00" }, // today
];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="datetime" widget="remaining_days" />
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"10/08/2017 11:00:00"
);
assert.containsOnce(target, "div.o_field_widget[name='datetime'] .o_datepicker");
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
await click(document.body, "a[data-action='close']");
await click(target, ".o_form_button_save");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"10/09/2017 11:00:00"
);
});
QUnit.test(
"RemainingDaysField on a datetime field in list view in UTC",
async function (assert) {
patchTimeZone(0);
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, datetime: "2017-10-08 20:00:00" }, // today
{ id: 2, datetime: "2017-10-09 08:00:00" }, // tomorrow
{ id: 3, datetime: "2017-10-07 18:00:00" }, // yesterday
{ id: 4, datetime: "2017-10-10 22:00:00" }, // + 2 days
{ id: 5, datetime: "2017-10-05 04:00:00" }, // - 3 days
{ id: 6, datetime: "2018-02-08 04:00:00" }, // + 4 months (diff >= 100 days)
{ id: 7, datetime: "2017-06-08 04:00:00" }, // - 4 months (diff >= 100 days)
{ id: 8, datetime: false },
];
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="datetime" widget="remaining_days" />
</tree>`,
});
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Today");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Tomorrow");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Yesterday");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "In 2 days");
assert.strictEqual(
target.querySelectorAll(".o_data_cell")[4].textContent,
"3 days ago"
);
assert.strictEqual(
target.querySelectorAll(".o_data_cell")[5].textContent,
"02/08/2018"
);
assert.strictEqual(
target.querySelectorAll(".o_data_cell")[6].textContent,
"06/08/2017"
);
assert.strictEqual(target.querySelectorAll(".o_data_cell")[7].textContent, "");
assert.hasAttrValue(
target.querySelector(".o_data_cell .o_field_widget div"),
"title",
"10/08/2017"
);
assert.hasClass(
target.querySelectorAll(".o_data_cell div div")[0],
"fw-bold text-warning"
);
assert.doesNotHaveClass(
target.querySelectorAll(".o_data_cell div div")[1],
"fw-bold text-warning text-danger"
);
assert.hasClass(
target.querySelectorAll(".o_data_cell div div")[2],
"fw-bold text-danger"
);
assert.doesNotHaveClass(
target.querySelectorAll(".o_data_cell div div")[3],
"fw-bold text-warning text-danger"
);
assert.hasClass(
target.querySelectorAll(".o_data_cell div div")[4],
"fw-bold text-danger"
);
assert.doesNotHaveClass(
target.querySelectorAll(".o_data_cell div div")[5],
"fw-bold text-warning text-danger"
);
assert.hasClass(
target.querySelectorAll(".o_data_cell div div")[6],
"fw-bold text-danger"
);
}
);
QUnit.test(
"RemainingDaysField on a datetime field in list view in UTC+6",
async function (assert) {
patchTimeZone(360);
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC+6
serverData.models.partner.records = [
{ id: 1, datetime: "2017-10-08 20:00:00" }, // tomorrow
{ id: 2, datetime: "2017-10-09 08:00:00" }, // tomorrow
{ id: 3, datetime: "2017-10-07 18:30:00" }, // today
{ id: 4, datetime: "2017-10-07 12:00:00" }, // yesterday
{ id: 5, datetime: "2017-10-09 20:00:00" }, // + 2 days
];
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="datetime" widget="remaining_days" />
</tree>`,
});
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Tomorrow");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Tomorrow");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Today");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "Yesterday");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[4].textContent, "In 2 days");
assert.hasAttrValue(
target.querySelector(".o_data_cell .o_field_widget div"),
"title",
"10/09/2017"
);
}
);
QUnit.test("RemainingDaysField on a date field in list view in UTC-6", async function (assert) {
patchTimeZone(-360);
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
serverData.models.partner.records = [
{ id: 1, date: "2017-10-08" }, // today
{ id: 2, date: "2017-10-09" }, // tomorrow
{ id: 3, date: "2017-10-07" }, // yesterday
{ id: 4, date: "2017-10-10" }, // + 2 days
{ id: 5, date: "2017-10-05" }, // - 3 days
];
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="date" widget="remaining_days" />
</tree>`,
});
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Today");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Tomorrow");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Yesterday");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "In 2 days");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[4].textContent, "3 days ago");
assert.hasAttrValue(
target.querySelector(".o_data_cell .o_field_widget div"),
"title",
"10/08/2017"
);
});
QUnit.test(
"RemainingDaysField on a datetime field in list view in UTC-8",
async function (assert) {
patchTimeZone(-560);
patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC-8
serverData.models.partner.records = [
{ id: 1, datetime: "2017-10-08 20:00:00" }, // today
{ id: 2, datetime: "2017-10-09 07:00:00" }, // today
{ id: 3, datetime: "2017-10-09 10:00:00" }, // tomorrow
{ id: 4, datetime: "2017-10-08 06:00:00" }, // yesterday
{ id: 5, datetime: "2017-10-07 02:00:00" }, // - 2 days
];
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="datetime" widget="remaining_days" />
</tree>`,
});
assert.strictEqual(target.querySelectorAll(".o_data_cell")[0].textContent, "Today");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[1].textContent, "Today");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[2].textContent, "Tomorrow");
assert.strictEqual(target.querySelectorAll(".o_data_cell")[3].textContent, "Yesterday");
assert.strictEqual(
target.querySelectorAll(".o_data_cell")[4].textContent,
"2 days ago"
);
}
);
});

View file

@ -0,0 +1,424 @@
/** @odoo-module **/
import { click, editSelect, editInput, getFixture, clickSave } 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" },
int_field: { string: "int_field", type: "integer", sortable: true },
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
product_id: { string: "Product", type: "many2one", relation: "product" },
color: {
type: "selection",
selection: [
["red", "Red"],
["black", "Black"],
],
default: "red",
string: "Color",
},
},
records: [
{
id: 1,
display_name: "first record",
int_field: 10,
trululu: 4,
},
{
id: 2,
display_name: "second record",
int_field: 9,
trululu: 1,
product_id: 37,
},
{
id: 4,
display_name: "aaa",
},
],
},
product: {
fields: {
name: { string: "Product Name", type: "char" },
},
records: [
{
id: 37,
display_name: "xphone",
},
{
id: 41,
display_name: "xpad",
},
],
},
},
};
setupViewRegistries();
});
QUnit.module("SelectionField");
QUnit.test("SelectionField in a list view", async function (assert) {
serverData.models.partner.records.forEach(function (r) {
r.color = "red";
});
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: '<tree string="Colors" editable="top"><field name="color"/></tree>',
});
assert.containsN(target, ".o_data_row", 3);
await click(target.querySelector(".o_data_cell"));
const td = target.querySelector("tbody tr.o_selected_row td:not(.o_list_record_selector)");
assert.containsOnce(td, "select", "td should have a child 'select'");
assert.strictEqual(td.children.length, 1, "select tag should be only child of td");
});
QUnit.test("SelectionField, edition and on many2one field", async function (assert) {
serverData.models.partner.onchanges = { product_id: function () {} };
serverData.models.partner.records[0].product_id = 37;
serverData.models.partner.records[0].trululu = false;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="product_id" widget="selection" />
<field name="trululu" widget="selection" />
<field name="color" widget="selection" />
</form>`,
mockRPC(route, { method }) {
assert.step(method);
},
});
assert.containsN(target, "select", 3);
assert.containsOnce(
target,
".o_field_widget[name='product_id'] select option[value='37']",
"should have fetched xphone option"
);
assert.containsOnce(
target,
".o_field_widget[name='product_id'] select option[value='41']",
"should have fetched xpad option"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name='product_id'] select").value,
"37",
"should have correct product_id value"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name='trululu'] select").value,
"false",
"should not have any value in trululu field"
);
await editSelect(target, ".o_field_widget[name='product_id'] select", "41");
assert.strictEqual(
target.querySelector(".o_field_widget[name='product_id'] select").value,
"41",
"should have a value of xphone"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name='color'] select").value,
`"red"`,
"should have correct value in color field"
);
assert.verifySteps(["get_views", "read", "name_search", "name_search", "onchange"]);
});
QUnit.test("unset selection field with 0 as key", async function (assert) {
// The server doesn't make a distinction between false value (the field
// is unset), and selection 0, as in that case the value it returns is
// false. So the client must convert false to value 0 if it exists.
serverData.models.partner.fields.selection = {
type: "selection",
selection: [
[0, "Value O"],
[1, "Value 1"],
],
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form edit="0"><field name="selection" /></form>',
});
assert.strictEqual(
target.querySelector(".o_field_widget").textContent,
"Value O",
"the displayed value should be 'Value O'"
);
assert.doesNotHaveClass(
target.querySelector(".o_field_widget"),
"o_field_empty",
"should not have class o_field_empty"
);
});
QUnit.test("unset selection field with string keys", async function (assert) {
// The server doesn't make a distinction between false value (the field
// is unset), and selection 0, as in that case the value it returns is
// false. So the client must convert false to value 0 if it exists. In
// this test, it doesn't exist as keys are strings.
serverData.models.partner.fields.selection = {
type: "selection",
selection: [
["0", "Value O"],
["1", "Value 1"],
],
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form edit="0"><field name="selection" /></form>',
});
assert.strictEqual(
target.querySelector(".o_field_widget").textContent,
"",
"there should be no displayed value"
);
assert.hasClass(
target.querySelector(".o_field_widget"),
"o_field_empty",
"should have class o_field_empty"
);
});
QUnit.test("unset selection on a many2one field", async function (assert) {
assert.expect(1);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="trululu" widget="selection" /></form>',
mockRPC(route, { args, method }) {
if (method === "write") {
assert.strictEqual(
args[1].trululu,
false,
"should send 'false' as trululu value"
);
}
},
});
await editSelect(target, ".o_form_view select", "false");
await clickSave(target);
});
QUnit.test("field selection with many2ones and special characters", async function (assert) {
// edit the partner with id=4
serverData.models.partner.records[2].display_name = "<span>hey</span>";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="trululu" widget="selection" /></form>',
});
assert.strictEqual(
target.querySelector("select option[value='4']").textContent,
"<span>hey</span>"
);
});
QUnit.test(
"SelectionField on a many2one: domain updated by an onchange",
async function (assert) {
assert.expect(4);
serverData.models.partner.onchanges = {
int_field() {},
};
let domain = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="int_field" />
<field name="trululu" widget="selection" />
</form>`,
mockRPC(route, { args, method }) {
if (method === "onchange") {
domain = [["id", "in", [10]]];
return Promise.resolve({
domain: {
trululu: domain,
},
});
}
if (method === "name_search") {
assert.deepEqual(args[1], domain, "sent domain should be correct");
}
},
});
assert.containsN(
target,
".o_field_widget[name='trululu'] option",
4,
"should be 4 options in the selection"
);
// trigger an onchange that will update the domain
await editInput(target, ".o_field_widget[name='int_field'] input", 2);
assert.containsOnce(
target,
".o_field_widget[name='trululu'] option",
"should be 1 option in the selection"
);
}
);
QUnit.test("required selection widget should not have blank option", async function (assert) {
serverData.models.partner.fields.feedback_value = {
type: "selection",
required: true,
selection: [
["good", "Good"],
["bad", "Bad"],
],
default: "good",
string: "Good",
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
</form>`,
});
assert.deepEqual(
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
(option) => option.style.display
),
["", "", ""]
);
assert.deepEqual(
[...target.querySelectorAll(".o_field_widget[name='feedback_value'] option")].map(
(option) => option.style.display
),
["none", "", ""]
);
// change value to update widget modifier values
await editSelect(target, ".o_field_widget[name='feedback_value'] select", '"bad"');
assert.deepEqual(
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
(option) => option.style.display
),
["none", "", ""]
);
});
QUnit.test(
"required selection widget should have only one blank option",
async function (assert) {
serverData.models.partner.fields.feedback_value = {
type: "selection",
required: true,
selection: [
["good", "Good"],
["bad", "Bad"],
],
default: "good",
string: "Good",
};
serverData.models.partner.fields.color.selection = [
[false, ""],
...serverData.models.partner.fields.color.selection,
];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
</form>`,
});
assert.containsN(
target.querySelector(".o_field_widget[name='color']"),
"option",
3,
"Three options in non required field (one blank option)"
);
// change value to update widget modifier values
await editSelect(target, ".o_field_widget[name='feedback_value'] select", '"bad"');
assert.deepEqual(
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
(option) => option.style.display
),
["none", "", ""]
);
}
);
QUnit.test("selection field with placeholder", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="trululu" widget="selection" placeholder="Placeholder"/>
</form>`,
});
const placeholderOption = target.querySelector(
".o_field_widget[name='trululu'] select option"
);
assert.strictEqual(placeholderOption.textContent, "Placeholder");
assert.strictEqual(placeholderOption.value, "false");
});
});

View file

@ -0,0 +1,337 @@
/** @odoo-module **/
import {
click,
clickSave,
editInput,
getFixture,
patchWithCleanup,
triggerEvent,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { NameAndSignature } from "@web/core/signature/name_and_signature";
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: "Name", type: "char" },
product_id: {
string: "Product Name",
type: "many2one",
relation: "product",
},
sign: { string: "Signature", type: "binary" },
},
records: [
{
id: 1,
display_name: "Pop's Chock'lit",
product_id: 7,
},
],
onchanges: {},
},
product: {
fields: {
name: { string: "Product Name", type: "char" },
},
records: [
{
id: 7,
display_name: "Veggie Burger",
},
],
},
},
};
setupViewRegistries();
});
QUnit.module("Signature Field");
QUnit.test("Set simple field in 'full_name' node option", async function (assert) {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
this._super.apply(this, arguments);
assert.step(this.props.signature.name);
},
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="sign" widget="signature" options="{'full_name': 'display_name'}" />
</form>`,
mockRPC: async (route) => {
if (route === "/web/sign/get_fonts/") {
return {};
}
},
});
assert.containsOnce(
target,
"div[name=sign] div.o_signature svg",
"should have a valid signature widget"
);
// Click on the widget to open signature modal
await click(target, "div[name=sign] div.o_signature");
assert.containsOnce(
target,
".modal .modal-body a.o_web_sign_auto_button",
'should open a modal with "Auto" button'
);
assert.verifySteps(["Pop's Chock'lit"]);
});
QUnit.test("Set m2o field in 'full_name' node option", async function (assert) {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
this._super.apply(this, arguments);
assert.step(this.props.signature.name);
},
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form>
<field name="product_id"/>
<field name="sign" widget="signature" options="{'full_name': 'product_id'}" />
</form>`,
mockRPC: async (route) => {
if (route === "/web/sign/get_fonts/") {
return {};
}
},
});
assert.containsOnce(
target,
"div[name=sign] div.o_signature svg",
"should have a valid signature widget"
);
// Click on the widget to open signature modal
await click(target, "div[name=sign] div.o_signature");
assert.containsOnce(
target,
".modal .modal-body a.o_web_sign_auto_button",
'should open a modal with "Auto" button'
);
assert.verifySteps(["Veggie Burger"]);
});
QUnit.test("Set size (width and height) in node option", async function (assert) {
serverData.models.partner.fields.sign2 = { string: "Signature", type: "binary" };
serverData.models.partner.fields.sign3 = { string: "Signature", type: "binary" };
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form>
<field name="sign" widget="signature" options="{'size': [150,'']}" />
<field name="sign2" widget="signature" options="{'size': ['',100]}" />
<field name="sign3" widget="signature" options="{'size': [120,130]}" />
</form>`,
mockRPC: async (route) => {
if (route === "/web/sign/get_fonts/") {
return {};
}
},
});
assert.containsN(target, ".o_signature", 3);
const sign = target.querySelector("[name='sign'] .o_signature");
assert.strictEqual(sign.style.width, "150px");
assert.strictEqual(sign.style.height, "50px");
const sign2 = target.querySelector("[name='sign2'] .o_signature");
assert.strictEqual(sign2.style.width, "300px");
assert.strictEqual(sign2.style.height, "100px");
const sign3 = target.querySelector("[name='sign3'] .o_signature");
assert.strictEqual(sign3.style.width, "120px");
assert.strictEqual(sign3.style.height, "40px");
});
QUnit.test(
"clicking save manually after changing signature should change the unique of the image src",
async function (assert) {
serverData.models.partner.fields.foo = { type: "char" };
serverData.models.partner.onchanges = { foo: () => {} };
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.sign = "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="sign" widget="signature" />
</form>`,
mockRPC(route, { method, args }) {
if (route === "/web/sign/get_fonts/") {
return {};
}
if (method === "write") {
assert.step("write");
args[1].__last_update = lastUpdates[index];
args[1].sign = "4 kb";
index++;
}
},
});
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659688620000"
);
await click(target, ".o_field_signature img", true);
assert.containsOnce(target, ".modal canvas");
let canvas = target.querySelector(".modal canvas");
canvas.setAttribute("width", "2px");
canvas.setAttribute("height", "2px");
let ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.moveTo(0, 0);
ctx.lineTo(0, 2);
ctx.stroke();
await triggerEvent(target, ".o_web_sign_signature", "change");
await click(target, ".modal-footer .btn-primary");
const MYB64 = `iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABRJREFUGFdjZGD438DAwNjACGMAACQlBAMW7JulAAAAAElFTkSuQmCC`;
assert.strictEqual(
target.querySelector("div[name=sign] img").dataset.src,
`data:image/png;base64,${MYB64}`
);
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
assert.strictEqual(
target.querySelector("div[name=sign] img").dataset.src,
`data:image/png;base64,${MYB64}`
);
await clickSave(target);
assert.verifySteps(["write"]);
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659692220000"
);
await click(target, ".o_field_signature img", true);
assert.containsOnce(target, ".modal canvas");
canvas = target.querySelector(".modal canvas");
canvas.setAttribute("width", "2px");
canvas.setAttribute("height", "2px");
ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.moveTo(0, 0);
ctx.lineTo(2, 0);
ctx.stroke();
await triggerEvent(target, ".o_web_sign_signature", "change");
await click(target, ".modal-footer .btn-primary");
const MYB64_2 = `iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABVJREFUGFdjZGD438DAwMDACCJAAAAWHgGCN0++VgAAAABJRU5ErkJggg==`;
assert.notOk(MYB64 === MYB64_2);
assert.strictEqual(
target.querySelector("div[name=sign] img").dataset.src,
`data:image/png;base64,${MYB64_2}`
);
await clickSave(target);
assert.verifySteps(["write"]);
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659695820000"
);
}
);
QUnit.test("save record with signature field modified by onchange", async function (assert) {
const MYB64 = `iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABRJREFUGFdjZGD438DAwNjACGMAACQlBAMW7JulAAAAAElFTkSuQmCC`;
serverData.models.partner.fields.foo = { type: "char" };
serverData.models.partner.onchanges = {
foo: (data) => {
data.sign = MYB64;
},
};
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.sign = "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="sign" widget="signature" />
</form>`,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.step("write");
args[1].__last_update = lastUpdates[index];
args[1].sign = "4 kb";
index++;
}
},
});
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659688620000"
);
await editInput(target, "[name='foo'] input", "grrr");
assert.strictEqual(
target.querySelector("div[name=sign] img").dataset.src,
`data:image/png;base64,${MYB64}`
);
await clickSave(target);
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659692220000"
);
assert.verifySteps(["write"]);
});
});

View file

@ -0,0 +1,187 @@
/** @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,
},
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
monetary: { string: "Monetary", type: "monetary" },
},
records: [
{
id: 1,
foo: "yop",
int_field: 10,
qux: 0.44444,
monetary: 9.999999,
},
],
},
},
};
setupViewRegistries();
});
QUnit.module("StatInfoField");
QUnit.test("StatInfoField formats decimal precision", async function (assert) {
// sometimes the round method can return numbers such as 14.000001
// when asked to round a number to 2 decimals, as such is the behaviour of floats.
// we check that even in that eventuality, only two decimals are displayed
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<button class="oe_stat_button" name="items" icon="fa-gear">
<field name="qux" widget="statinfo" />
</button>
<button class="oe_stat_button" name="money" icon="fa-money">
<field name="monetary" widget="statinfo" />
</button>
</form>`,
});
// formatFloat renders according to this.field.digits
assert.strictEqual(
target.querySelectorAll(".oe_stat_button .o_field_widget .o_stat_value")[0].textContent,
"0.4",
"Default precision should be [16,1]"
);
assert.strictEqual(
target.querySelectorAll(".oe_stat_button .o_field_widget .o_stat_value")[1].textContent,
"10.00",
"Currency decimal precision should be 2"
);
});
QUnit.test("StatInfoField in form view", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
<field name="int_field" widget="statinfo" />
</button>
</div>
</form>`,
});
assert.containsOnce(
target,
".oe_stat_button .o_field_widget .o_stat_info",
"should have one stat button"
);
assert.strictEqual(
target.querySelector(".oe_stat_button .o_field_widget .o_stat_value").textContent,
"10",
"should have 10 as value"
);
assert.strictEqual(
target.querySelector(".oe_stat_button .o_field_widget .o_stat_text").textContent,
"int_field",
"should have 'int_field' as text"
);
});
QUnit.test("StatInfoField in form view with specific label_field", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
<field string="Useful stat button" name="int_field" widget="statinfo" options="{'label_field': 'foo'}" />
</button>
</div>
<group>
<field name="foo" invisible="1" />
</group>
</sheet>
</form>`,
});
assert.containsOnce(
target,
".oe_stat_button .o_field_widget .o_stat_info",
"should have one stat button"
);
assert.strictEqual(
target.querySelector(".oe_stat_button .o_field_widget .o_stat_value").textContent,
"10",
"should have 10 as value"
);
assert.strictEqual(
target.querySelector(".oe_stat_button .o_field_widget .o_stat_text").textContent,
"yop",
"should have 'yop' as text, since it is the value of field foo"
);
});
QUnit.test("StatInfoField in form view with no label", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="items" type="object" icon="fa-gear">
<field string="Useful stat button" name="int_field" widget="statinfo" nolabel="1" />
</button>
</div>
</sheet>
</form>`,
});
assert.containsOnce(
target,
".oe_stat_button .o_field_widget .o_stat_info",
"should have one stat button"
);
assert.strictEqual(
target.querySelector(".oe_stat_button .o_field_widget .o_stat_value").textContent,
"10",
"should have 10 as value"
);
assert.containsNone(
target,
".oe_stat_button .o_field_widget .o_stat_text",
"should not have any label"
);
});
});

View file

@ -0,0 +1,598 @@
/** @odoo-module **/
import { click, getFixture, nextTick, triggerHotkey } 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",
},
sequence: { type: "integer", string: "Sequence", searchable: true },
selection: {
string: "Selection",
type: "selection",
selection: [
["normal", "Normal"],
["blocked", "Blocked"],
["done", "Done"],
],
},
},
records: [
{
id: 1,
foo: "yop",
selection: "blocked",
},
{
id: 2,
foo: "blip",
selection: "normal",
},
{
id: 4,
foo: "abc",
selection: "done",
},
{ id: 3, foo: "gnap" },
{ id: 5, foo: "blop" },
],
},
},
};
setupViewRegistries();
});
QUnit.module("StateSelectionField");
QUnit.test("StateSelectionField in form view", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="state_selection"/>
</group>
</sheet>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
"should have one red status since selection is the second, blocked state"
);
assert.containsNone(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
"should not have one green status since selection is the second, blocked state"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
assert.strictEqual(
target.querySelector(".o_field_state_selection .dropdown-toggle").dataset.tooltip,
"Blocked",
"tooltip attribute has the right text"
);
// Click on the status button to make the dropdown appear
await click(target, ".o_field_widget.o_field_state_selection .o_status");
assert.containsOnce(document.body, ".dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
);
// Click on the first option, "Normal"
await click(target.querySelector(".dropdown-menu .dropdown-item"));
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
assert.containsNone(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
"should not have one red status since selection is the first, normal state"
);
assert.containsNone(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
"should not have one green status since selection is the first, normal state"
);
assert.containsOnce(
target,
".o_field_widget.o_field_state_selection span.o_status",
"should have one grey status since selection is the first, normal state"
);
assert.containsNone(target, ".dropdown-menu", "there should still not be a dropdown");
assert.containsNone(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
"should still not have one red status since selection is the first, normal state"
);
assert.containsNone(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
"should still not have one green status since selection is the first, normal state"
);
assert.containsOnce(
target,
".o_field_widget.o_field_state_selection span.o_status",
"should still have one grey status since selection is the first, normal state"
);
// Click on the status button to make the dropdown appear
await click(target, ".o_field_widget.o_field_state_selection .o_status");
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
);
// Click on the last option, "Done"
await click(target, ".dropdown-menu .dropdown-item:last-child");
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
assert.containsNone(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
"should not have one red status since selection is the third, done state"
);
assert.containsOnce(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
"should have one green status since selection is the third, done state"
);
// save
await click(target.querySelector(".o_form_button_save"));
assert.containsNone(
target,
".dropdown-menu",
"there should still not be a dropdown anymore"
);
assert.containsNone(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_red",
"should still not have one red status since selection is the third, done state"
);
assert.containsOnce(
target,
".o_field_widget.o_field_state_selection span.o_status.o_status_green",
"should still have one green status since selection is the third, done state"
);
});
QUnit.test("StateSelectionField with readonly modifier", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="selection" widget="state_selection" readonly="1"/></form>',
resId: 1,
});
assert.hasClass(target.querySelector(".o_field_state_selection"), "o_readonly_modifier");
assert.isNotVisible(target.querySelector(".dropdown-menu"));
await click(target, ".o_field_state_selection span.o_status");
assert.isNotVisible(target.querySelector(".dropdown-menu"));
});
QUnit.test("StateSelectionField for list view with hide_label option", async function (assert) {
Object.assign(serverData.models.partner.fields, {
graph_type: {
string: "Graph Type",
type: "selection",
selection: [
["line", "Line"],
["bar", "Bar"],
],
},
});
serverData.models.partner.records[0].graph_type = "bar";
serverData.models.partner.records[1].graph_type = "line";
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="graph_type" widget="state_selection" options="{'hide_label': True}"/>
<field name="selection" widget="state_selection"/>
</tree>`,
});
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
10,
"should have ten status selection widgets"
);
const selection = Array.from(
target.querySelectorAll(
".o_state_selection_cell .o_field_state_selection[name=selection] span.o_status_label"
)
);
assert.strictEqual(selection.length, 5, "should have five label on selection widgets");
assert.strictEqual(
selection.filter((el) => el.textContent === "Done").length,
1,
"should have one Done status label"
);
assert.strictEqual(
selection.filter((el) => el.textContent === "Normal").length,
3,
"should have three Normal status label"
);
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection[name=graph_type] span.o_status",
5,
"should have five status selection widgets"
);
assert.containsNone(
target,
".o_state_selection_cell .o_field_state_selection[name=graph_type] span.o_status_label",
"should not have status label in selection widgets"
);
});
QUnit.test("StateSelectionField 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="state_selection"/>
</tree>`,
});
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
5,
"should have five status selection widgets"
);
assert.containsOnce(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
"should have one red status"
);
assert.containsOnce(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
"should have one green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
// Click on the status button to make the dropdown appear
let cell = target.querySelector("tbody td.o_state_selection_cell");
await click(
target.querySelector(".o_state_selection_cell .o_field_state_selection span.o_status")
);
assert.doesNotHaveClass(
cell.parentElement,
"o_selected_row",
"should not be in edit mode since we clicked on the state selection widget"
);
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
);
// Click on the first option, "Normal"
await click(target.querySelector(".dropdown-menu .dropdown-item"));
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
5,
"should still have five status selection widgets"
);
assert.containsNone(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
"should now have no red status"
);
assert.containsOnce(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
"should still have one green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
assert.containsNone(target, "tr.o_selected_row", "should not be in edit mode");
// switch to edit mode and check the result
cell = target.querySelector("tbody td.o_state_selection_cell");
await click(cell);
assert.hasClass(cell.parentElement, "o_selected_row", "should now be in edit mode");
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
5,
"should still have five status selection widgets"
);
assert.containsNone(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
"should now have no red status"
);
assert.containsOnce(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
"should still have one green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
// Click on the status button to make the dropdown appear
await click(
target.querySelector(".o_state_selection_cell .o_field_state_selection span.o_status")
);
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
);
// Click on another row
const lastCell = target.querySelectorAll("tbody td.o_state_selection_cell")[4];
await click(lastCell);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
const firstCell = target.querySelector("tbody td.o_state_selection_cell");
assert.doesNotHaveClass(
firstCell.parentElement,
"o_selected_row",
"first row should not be in edit mode anymore"
);
assert.hasClass(
lastCell.parentElement,
"o_selected_row",
"last row should be in edit mode"
);
// Click on the fourth status button to make the dropdown appear
await click(
target.querySelectorAll(
".o_state_selection_cell .o_field_state_selection span.o_status"
)[3]
);
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
);
// Click on the last option, "Done"
await click(target, ".dropdown-menu .dropdown-item:last-child");
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown anymore");
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
5,
"should still have five status selection widgets"
);
assert.containsNone(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
"should still have no red status"
);
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
2,
"should now have two green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
// save
await click(target.querySelector(".o_list_button_save"));
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
5,
"should have five status selection widgets"
);
assert.containsNone(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_red",
"should have no red status"
);
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status.o_status_green",
2,
"should have two green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
});
QUnit.test(
'StateSelectionField edited by the smart action "Set kanban state..."',
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="selection" widget="state_selection"/>
</form>`,
resId: 1,
});
assert.containsOnce(target, ".o_status_red");
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")]
.map((el) => el.textContent)
.indexOf("Set kanban state...ALT + SHIFT + R");
assert.ok(idx >= 0);
await click([...target.querySelectorAll(".o_command")][idx]);
await nextTick();
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Normal", "Blocked", "Done"]
);
await click(target, "#o_command_2");
await nextTick();
assert.containsOnce(target, ".o_status_green");
}
);
QUnit.test("StateSelectionField uses legend_* fields", async function (assert) {
serverData.models.partner.fields.legend_normal = { type: "char" };
serverData.models.partner.fields.legend_blocked = { type: "char" };
serverData.models.partner.fields.legend_done = { type: "char" };
serverData.models.partner.records[0].legend_normal = "Custom normal";
serverData.models.partner.records[0].legend_blocked = "Custom blocked";
serverData.models.partner.records[0].legend_done = "Custom done";
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="legend_normal" invisible="1" />
<field name="legend_blocked" invisible="1" />
<field name="legend_done" invisible="1" />
<field name="selection" widget="state_selection"/>
</group>
</sheet>
</form>`,
resId: 1,
});
await click(target, ".o_status");
let dropdownItemTexts = [...target.querySelectorAll(".dropdown-item")].map(
(el) => el.textContent
);
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom done"]);
await click(target.querySelector(".dropdown-item .o_status"));
await click(target, ".o_status");
dropdownItemTexts = [...target.querySelectorAll(".dropdown-item")].map(
(el) => el.textContent
);
assert.deepEqual(dropdownItemTexts, ["Custom blocked", "Custom done"]);
});
QUnit.test("works when required in a readonly view ", async function (assert) {
serverData.models.partner.records[0].selection = "normal";
serverData.models.partner.records = [serverData.models.partner.records[0]];
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="selection" widget="state_selection" required="1"/>
</div>
</t>
</templates>
</kanban>`,
mockRPC: (route, args, performRPC) => {
if (route === "/web/dataset/call_kw/partner/write") {
assert.step("write");
}
return performRPC(route, args);
},
});
await click(target, ".o_field_state_selection button");
const doneItem = target.querySelectorAll(".dropdown-item")[1]; // item "done";
await click(doneItem);
assert.verifySteps(["write"]);
assert.hasClass(target.querySelector(".o_field_state_selection span"), "o_status_green");
});
QUnit.test(
"StateSelectionField - auto save record when field toggled",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="state_selection"/>
</group>
</sheet>
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
}
},
});
await click(target, ".o_field_widget.o_field_state_selection .o_status");
await click(target, ".dropdown-menu .dropdown-item:last-child");
assert.verifySteps(["write"]);
}
);
QUnit.test(
"StateSelectionField - prevent auto save with autosave option",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="selection" widget="state_selection" options="{'autosave': False}"/>
</group>
</sheet>
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
}
},
});
await click(target, ".o_field_widget.o_field_state_selection .o_status");
await click(target, ".dropdown-menu .dropdown-item:last-child");
assert.verifySteps([]);
}
);
});

View file

@ -0,0 +1,684 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
import {
click,
editInput,
getFixture,
nextTick,
patchWithCleanup,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { EventBus } from "@odoo/owl";
let serverData;
let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
foo: { string: "Foo", type: "char", default: "My little Foo Value" },
bar: { string: "Bar", type: "boolean", default: true },
int_field: { string: "int_field", type: "integer", sortable: true },
qux: { string: "Qux", type: "float", digits: [16, 1] },
p: {
string: "one2many field",
type: "one2many",
relation: "partner",
relation_field: "trululu",
},
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
product_id: { string: "Product", type: "many2one", relation: "product" },
color: {
type: "selection",
selection: [
["red", "Red"],
["black", "Black"],
],
default: "red",
string: "Color",
},
user_id: { string: "User", type: "many2one", relation: "user" },
},
records: [
{
id: 1,
display_name: "first record",
bar: true,
foo: "yop",
int_field: 10,
qux: 0.44,
p: [],
trululu: 4,
user_id: 17,
},
{
id: 2,
display_name: "second record",
bar: true,
foo: "blip",
int_field: 9,
qux: 13,
p: [],
trululu: 1,
product_id: 37,
user_id: 17,
},
{
id: 4,
display_name: "aaa",
bar: false,
},
],
},
product: {
fields: {
name: { string: "Product Name", type: "char" },
},
records: [
{
id: 37,
display_name: "xphone",
},
{
id: 41,
display_name: "xpad",
},
],
},
user: {
fields: {
name: { string: "Name", type: "char" },
partner_ids: {
string: "one2many partners field",
type: "one2many",
relation: "partner",
relation_field: "user_id",
},
},
records: [
{
id: 17,
name: "Aline",
partner_ids: [1, 2],
},
{
id: 19,
name: "Christine",
},
],
},
},
};
setupViewRegistries();
});
QUnit.module("StatusBarField");
QUnit.test("static statusbar widget on many2one field", async function (assert) {
serverData.models.partner.fields.trululu.domain = "[('bar', '=', True)]";
serverData.models.partner.records[1].bar = false;
let count = 0;
let fieldsFetched = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" />
</header>
</form>`,
mockRPC(route, { method, kwargs }) {
if (method === "search_read") {
count++;
fieldsFetched = kwargs.fields;
}
},
});
assert.strictEqual(
count,
1,
"once search_read should have been done to fetch the relational values"
);
assert.deepEqual(
fieldsFetched,
["display_name"],
"search_read should only fetch field display_name"
);
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 2);
assert.containsN(target, ".o_statusbar_status button:disabled", 2);
assert.hasClass(
target.querySelector('.o_statusbar_status button[data-value="4"]'),
"o_arrow_button_current"
);
});
QUnit.test(
"folded statusbar widget on selection field has selected value in the toggler",
async function (assert) {
registry.category("services").add("ui", {
start(env) {
Object.defineProperty(env, "isSmall", {
value: true,
});
return {
bus: new EventBus(),
size: 0,
isSmall: true,
};
},
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="color" widget="statusbar" />
</header>
</form>`,
});
assert.containsOnce(target, ".o_statusbar_status button.dropdown-toggle:contains(Red)");
}
);
QUnit.test("static statusbar widget on many2one field with domain", async function (assert) {
assert.expect(1);
patchWithCleanup(session, { uid: 17 });
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" domain="[('user_id', '=', uid)]" />
</header>
</form>`,
mockRPC(route, { method, kwargs }) {
if (method === "search_read") {
assert.deepEqual(
kwargs.domain,
["|", ["id", "=", 4], ["user_id", "=", 17]],
"search_read should sent the correct domain"
);
}
},
});
});
QUnit.test("clickable statusbar widget on many2one field", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
</header>
</form>`,
});
assert.hasClass(
target.querySelector(".o_statusbar_status button[data-value='4']"),
"o_arrow_button_current"
);
assert.hasClass(
target.querySelector(".o_statusbar_status button[data-value='4']"),
"disabled"
);
const clickableButtons = target.querySelectorAll(
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
);
assert.strictEqual(clickableButtons.length, 2);
await click(clickableButtons[clickableButtons.length - 1]); // (last is visually the first here (css))
assert.hasClass(
target.querySelector(".o_statusbar_status button[data-value='1']"),
"o_arrow_button_current"
);
assert.hasClass(
target.querySelector(".o_statusbar_status button[data-value='1']"),
"disabled"
);
});
QUnit.test("statusbar with no status", async function (assert) {
serverData.models.product.records = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" />
</header>
</form>`,
});
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
assert.strictEqual(
target.querySelector(".o_statusbar_status").children.length,
0,
"statusbar widget should be empty"
);
});
QUnit.test("statusbar with tooltip for help text", async function (assert) {
serverData.models.partner.fields.product_id.help = "some info about the field";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" />
</header>
</form>`,
});
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
const tooltipInfo = target.querySelector(".o_field_statusbar").attributes[
"data-tooltip-info"
];
assert.strictEqual(
JSON.parse(tooltipInfo.value).field.help,
"some info about the field",
"tooltip text is present on the field"
);
});
QUnit.test("statusbar with required modifier", async function (assert) {
const mock = () => {
assert.step("Show error message");
return () => {};
};
registry.category("services").add("notification", makeFakeNotificationService(mock), {
force: true,
});
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" required="1"/>
</header>
</form>`,
});
await click(target, ".o_form_button_save");
assert.containsOnce(target, ".o_form_editable", "view should still be in edit");
assert.verifySteps(
["Show error message"],
"should display an 'invalid fields' notification"
);
});
QUnit.test("statusbar with no value in readonly", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" />
</header>
</form>`,
});
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
assert.containsN(target, ".o_statusbar_status button:visible", 2);
});
QUnit.test("statusbar with domain but no value (create mode)", async function (assert) {
serverData.models.partner.fields.trululu.domain = "[('bar', '=', True)]";
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" />
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button:disabled", 2);
});
QUnit.test(
"clickable statusbar should change m2o fetching domain in edit mode",
async function (assert) {
serverData.models.partner.fields.trululu.domain = "[('bar', '=', True)]";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 3);
const buttons = target.querySelectorAll(
".o_statusbar_status button:not(.dropdown-toggle)"
);
await click(buttons[buttons.length - 1]);
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 2);
}
);
QUnit.test(
"statusbar fold_field option and statusbar_visible attribute",
async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
serverData.models.partner.records[0].bar = false;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'fold_field': 'bar'}" />
<field name="color" widget="statusbar" statusbar_visible="red" />
</header>
</form>`,
});
await click(target, ".o_statusbar_status .dropdown-toggle");
const status = target.querySelectorAll(".o_statusbar_status");
assert.containsOnce(status[0], ".dropdown-item.disabled");
assert.containsOnce(status[status.length - 1], "button.disabled");
}
);
QUnit.test("statusbar: choose an item from the 'More' menu", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
serverData.models.partner.records[0].bar = false;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': '1', 'fold_field': 'bar'}" />
</header>
</form>`,
});
assert.strictEqual(
target.querySelector("[aria-checked='true']").textContent,
"aaa",
"default status is 'aaa'"
);
assert.strictEqual(
document
.querySelector(".o_statusbar_status .dropdown-toggle.o_arrow_button")
.textContent.trim(),
"More",
"button has the correct text"
);
await click(target, ".o_statusbar_status .dropdown-toggle");
await click(target, ".o-dropdown .dropdown-item");
assert.strictEqual(
target.querySelector("[aria-checked='true']").textContent,
"second record",
"status has changed to the selected dropdown item"
);
});
QUnit.test("statusbar with dynamic domain", async function (assert) {
serverData.models.partner.fields.trululu.domain = "[('int_field', '>', qux)]";
serverData.models.partner.records[2].int_field = 0;
let rpcCount = 0;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" />
</header>
<field name="qux" />
<field name="foo" />
</form>`,
mockRPC(route, { method }) {
if (method === "search_read") {
rpcCount++;
}
},
});
assert.containsN(target, ".o_statusbar_status button.disabled", 3);
assert.strictEqual(rpcCount, 1, "should have done 1 search_read rpc");
await editInput(target, ".o_field_widget[name='qux'] input", 9.5);
assert.containsN(target, ".o_statusbar_status button.disabled", 2);
assert.strictEqual(rpcCount, 2, "should have done 1 more search_read rpc");
await editInput(target, ".o_field_widget[name='qux'] input", "hey");
assert.strictEqual(rpcCount, 2, "should not have done 1 more search_read rpc");
});
QUnit.test('statusbar edited by the smart action "Move to stage..."', async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': '1'}"/>
</header>
</form>`,
resId: 1,
});
assert.containsOnce(target, ".o_field_widget");
triggerHotkey("control+k");
await nextTick();
const movestage = target.querySelectorAll(".o_command");
const idx = [...movestage]
.map((el) => el.textContent)
.indexOf("Move to Trululu...ALT + SHIFT + X");
assert.ok(idx >= 0);
await click(movestage[idx]);
await nextTick();
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["first record", "second record", "aaa"]
);
await click(target, "#o_command_2");
});
QUnit.test(
'smart action "Move to stage..." is unavailable if readonly',
async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" readonly="1"/>
</header>
</form>`,
resId: 1,
});
assert.containsOnce(target, ".o_field_widget");
triggerHotkey("control+k");
await nextTick();
const movestage = target.querySelectorAll(".o_command");
const idx = [...movestage]
.map((el) => el.textContent)
.indexOf("Move to Trululu...ALT + SHIFT + X");
assert.ok(idx < 0);
}
);
QUnit.test("hotkey is unavailable if readonly", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" readonly="1"/>
</header>
</form>`,
resId: 1,
});
assert.containsOnce(target, ".o_field_widget");
triggerHotkey("alt+shift+x");
await nextTick();
assert.containsNone(target, ".modal", "command palette should not open");
});
QUnit.test("auto save record when field toggled", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="trululu" widget="statusbar" options="{'clickable': 1}" />
</header>
</form>`,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
}
},
});
const clickableButtons = target.querySelectorAll(
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
);
await click(clickableButtons[clickableButtons.length - 1]);
assert.verifySteps(["write"]);
});
QUnit.test(
"clickable statusbar with readonly modifier set to false is editable",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 2,
serverData,
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': false}"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button:visible", 2);
assert.containsNone(target, ".o_statusbar_status button.disabled[disabled]:visible");
}
);
QUnit.test(
"clickable statusbar with readonly modifier set to true is not editable",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 2,
serverData,
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': true}"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
}
);
QUnit.test(
"non-clickable statusbar with readonly modifier set to false is not editable",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 2,
serverData,
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': false}" attrs="{'readonly': false}"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
}
);
});

View file

@ -0,0 +1,595 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import {
click,
clickCreate,
clickSave,
editInput,
getFixture,
triggerEvent,
} 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(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
foo: {
string: "Foo",
type: "char",
default: "My little Foo Value",
searchable: true,
trim: true,
},
bar: { string: "Bar", type: "boolean", default: true, searchable: true },
txt: {
string: "txt",
type: "text",
},
int_field: {
string: "int_field",
type: "integer",
sortable: true,
searchable: true,
},
qux: { string: "Qux", type: "float", digits: [16, 1], searchable: true },
},
records: [
{
id: 1,
bar: true,
foo: "yop",
int_field: 10,
qux: 0.44444,
txt: "some text",
},
],
},
},
};
setupViewRegistries();
});
QUnit.module("TextField");
QUnit.test("text fields are correctly rendered", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="foo"/></form>',
});
const textarea = target.querySelector(".o_field_text textarea");
assert.ok(textarea, "should have a text area");
assert.strictEqual(textarea.value, "yop", "should still be 'yop' in edit");
await editInput(textarea, null, "hello");
assert.strictEqual(textarea.value, "hello", "should be 'hello' after first edition");
await editInput(textarea, null, "hello world");
assert.strictEqual(
textarea.value,
"hello world",
"should be 'hello world' after second edition"
);
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_text textarea").value,
"hello world",
"should be 'hello world' after save"
);
});
QUnit.test("text fields in edit mode have correct height", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
serverData.models.partner.records[0].foo = "f\nu\nc\nk\nm\ni\nl\ng\nr\no\nm";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="foo"/></form>',
});
const textarea = target.querySelector(".o_field_text textarea");
assert.strictEqual(
textarea.clientHeight,
textarea.scrollHeight - Math.abs(textarea.scrollTop),
"textarea should not have a scroll bar"
);
});
QUnit.test("text fields in edit mode, no vertical resize", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="txt"/></form>',
});
assert.strictEqual(
window.getComputedStyle(target.querySelector("textarea")).resize,
"none",
"should not have vertical resize"
);
});
QUnit.test("text fields should have correct height after onchange", async function (assert) {
const damnLongText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Donec est massa, gravida eget dapibus ac, eleifend eget libero.
Suspendisse feugiat sed massa eleifend vestibulum. Sed tincidunt
velit sed lacinia lacinia. Nunc in fermentum nunc. Vestibulum ante
ipsum primis in faucibus orci luctus et ultrices posuere cubilia
Curae; Nullam ut nisi a est ornare molestie non vulputate orci.
Nunc pharetra porta semper. Mauris dictum eu nulla a pulvinar. Duis
eleifend odio id ligula congue sollicitudin. Curabitur quis aliquet
nunc, ut aliquet enim. Suspendisse malesuada felis non metus
efficitur aliquet.`;
serverData.models.partner.records[0].txt = damnLongText;
serverData.models.partner.records[0].bar = false;
serverData.models.partner.onchanges = {
bar(obj) {
obj.txt = damnLongText;
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="bar" />
<field name="txt" attrs="{'invisible': [('bar', '=', True)]}" />
</form>`,
});
let textarea = target.querySelector(".o_field_widget[name='txt'] textarea");
const initialHeight = textarea.offsetHeight;
await editInput(textarea, null, "Short value");
assert.ok(textarea.offsetHeight < initialHeight, "Textarea height should have shrank");
await click(target, ".o_field_boolean[name='bar'] input");
await click(target, ".o_field_boolean[name='bar'] input");
textarea = target.querySelector(".o_field_widget[name='txt'] textarea");
assert.strictEqual(textarea.offsetHeight, initialHeight, "Textarea height should be reset");
});
QUnit.test("text fields in editable list have correct height", async function (assert) {
assert.expect(2);
serverData.models.partner.records[0].txt = "a\nb\nc\nd\ne\nf";
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: '<list editable="top"><field name="foo"/><field name="txt"/></list>',
});
// Click to enter edit: in this test we specifically do not set
// the focus on the textarea by clicking on another column.
// The main goal is to test the resize is actually triggered in this
// particular case.
await click(target.querySelectorAll(".o_data_cell")[1]);
const textarea = target.querySelector("textarea:first-child");
// make sure the correct data is there
assert.strictEqual(textarea.value, serverData.models.partner.records[0].txt);
// make sure there is no scroll bar
assert.strictEqual(
textarea.clientHeight,
textarea.scrollHeight,
"textarea should not have a scroll bar"
);
});
QUnit.test("text fields in edit mode should resize on reset", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
serverData.models.partner.onchanges = {
bar(obj) {
obj.foo = "a\nb\nc\nd\ne\nf";
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="bar" />
<field name="foo" />
</form>`,
});
// trigger a textarea reset (through onchange) by clicking the box
// then check there is no scroll bar
await click(target, "div[name='bar'] input");
const textarea = target.querySelector("textarea");
assert.strictEqual(
textarea.clientHeight,
textarea.scrollHeight,
"textarea should not have a scroll bar"
);
});
QUnit.test("set row on text fields", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="foo" rows="4"/>
</form>`,
});
const textarea = target.querySelector("textarea");
assert.strictEqual(
textarea.rows,
4,
"rowCount should be the one set on the field",
);
});
QUnit.test(
"autoresize of text fields is done when switching to edit mode",
async function (assert) {
serverData.models.partner.fields.text_field = { string: "Text field", type: "text" };
serverData.models.partner.fields.text_field.default = "some\n\nmulti\n\nline\n\ntext\n";
serverData.models.partner.records[0].text_field = "a\nb\nc\nd\ne\nf";
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="display_name"/>
<field name="text_field"/>
</form>`,
resId: 1,
});
// ensure that autoresize is correctly done
let height = target.querySelector(".o_field_widget[name=text_field] textarea")
.offsetHeight;
// focus the field to manually trigger autoresize
await triggerEvent(target, ".o_field_widget[name=text_field] textarea", "focus");
assert.strictEqual(
target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight,
height,
"autoresize should have been done automatically at rendering"
);
// next assert simply tries to ensure that the textarea isn't stucked to
// its minimal size, even after being focused
assert.ok(height > 80, "textarea should have an height of at least 80px");
// create a new record to ensure that autoresize is correctly done
await clickCreate(target);
height = target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight;
// focus the field to manually trigger autoresize
await triggerEvent(target, ".o_field_widget[name=text_field] textarea", "focus");
assert.strictEqual(
target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight,
height,
"autoresize should have been done automatically at rendering"
);
assert.ok(height > 80, "textarea should have an height of at least 80px");
}
);
QUnit.test("autoresize of text fields is done on notebook page show", async function (assert) {
serverData.models.partner.fields.text_field = { string: "Text field", type: "text" };
serverData.models.partner.fields.text_field.default = "some\n\nmulti\n\nline\n\ntext\n";
serverData.models.partner.records[0].text_field = "a\nb\nc\nd\ne\nf";
serverData.models.partner.fields.text_field_empty = {
string: "Text field",
type: "text",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<notebook>
<page string="First Page">
<field name="foo"/>
</page>
<page string="Second Page">
<field name="text_field"/>
</page>
<page string="Third Page">
<field name="text_field_empty"/>
</page>
</notebook>
</sheet>
</form>`,
resId: 1,
});
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[0], "active");
await click(target.querySelectorAll(".o_notebook .nav .nav-link")[1]);
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[1], "active");
let height = target.querySelector(".o_field_widget[name=text_field] textarea").offsetHeight;
assert.ok(height > 80, "textarea should have an height of at least 80px");
await click(target.querySelectorAll(".o_notebook .nav .nav-link")[2]);
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[2], "active");
height = target.querySelector(".o_field_widget[name=text_field_empty] textarea")
.offsetHeight;
assert.strictEqual(height, 50, "empty textarea should have height of 50px");
});
QUnit.test("text field translatable", async function (assert) {
assert.expect(3);
serverData.models.partner.fields.txt.translate = true;
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
force: true,
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="txt" />
</group>
</sheet>
</form>`,
mockRPC(route, { args, method }) {
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/get_field_translations") {
return Promise.resolve([
[
{ lang: "en_US", source: "yop", value: "yop" },
{ lang: "fr_BE", source: "yop", value: "valeur français" },
],
{ translation_type: "text", translation_show_source: false },
]);
}
},
});
assert.hasClass(target.querySelector("[name=txt] textarea"), "o_field_translate");
assert.containsOnce(
target,
".o_field_text .btn.o_field_translate",
"should have a translate button"
);
await click(target, ".o_field_text .btn.o_field_translate");
assert.containsOnce(target, ".modal", "there should be a translation modal");
});
QUnit.test("text field translatable in create mode", async function (assert) {
serverData.models.partner.fields.txt.translate = true;
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
force: true,
});
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="txt" />
</group>
</sheet>
</form>`,
});
assert.containsOnce(
target,
".o_field_text .btn.o_field_translate",
"should have a translate button in create mode"
);
});
QUnit.test("text field translatable on notebook page", async function (assert) {
serverData.models.partner.fields.txt.translate = true;
serviceRegistry.add("localization", makeFakeLocalizationService({ multiLang: true }), {
force: true,
});
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<notebook>
<page string="First Page">
<field name="txt"/>
</page>
</notebook>
</sheet>
</form>`,
resId: 1,
mockRPC(route, { args, method }) {
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/get_field_translations") {
return Promise.resolve([
[
{ lang: "en_US", source: "yop", value: "yop" },
{ lang: "fr_BE", source: "yop", value: "valeur français" },
],
{ translation_type: "text", translation_show_source: false },
]);
}
},
});
assert.hasClass(target.querySelectorAll(".o_notebook .nav .nav-link")[0], "active");
assert.hasClass(target.querySelector("[name=txt] textarea"), "o_field_translate");
assert.strictEqual(
target.querySelector("[name=txt] textarea").nextElementSibling.textContent,
"EN",
"The input should be preceded by a translate button"
);
await click(target, ".o_field_text .btn.o_field_translate");
assert.containsOnce(target, ".modal", "there should be a translation modal");
});
QUnit.test(
"go to next line (and not the next row) when pressing enter",
async function (assert) {
serverData.models.partner.fields.foo.type = "text";
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<list editable="top">
<field name="int_field" />
<field name="foo" />
<field name="qux" />
</list>`,
});
await click(target.querySelector("tbody tr:first-child .o_list_text"));
const textarea = target.querySelector("textarea.o_input");
assert.containsOnce(target, textarea, "should have a text area");
assert.strictEqual(textarea.value, "yop", 'should still be "yop" in edit');
assert.strictEqual(
target.querySelector("textarea"),
document.activeElement,
"text area should have the focus"
);
// click on enter
await triggerEvent(textarea, null, "keydown", { key: "Enter" });
await triggerEvent(textarea, null, "keyup", { key: "Enter" });
assert.strictEqual(
target.querySelector("textarea"),
document.activeElement,
"text area should still have the focus"
);
}
);
// Firefox-specific
// Copying from <div style="white-space:pre-wrap"> does not keep line breaks
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1390115
QUnit.test(
"copying text fields in RO mode should preserve line breaks",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form edit="0">
<sheet>
<group>
<field name="txt"/>
</group>
</sheet>
</form>`,
resId: 1,
});
// Copying from a div tag with white-space:pre-wrap doesn't work in Firefox
assert.strictEqual(
target.querySelector('[name="txt"]').firstElementChild.tagName.toLowerCase(),
"span",
"the field contents should be surrounded by a span tag"
);
}
);
QUnit.test("text field rendering in list view", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "partner",
arch: '<tree><field name="txt"/></tree>',
});
assert.containsOnce(
target,
"tbody td.o_list_text",
"should have a td with the .o_list_text class"
);
});
QUnit.test("field text in editable list view", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: '<tree editable="top"><field name="foo"/></tree>',
});
await click(target.querySelector(".o_list_button_add"));
assert.strictEqual(
target.querySelector("textarea"),
document.activeElement,
"text area should have the focus"
);
});
});

View file

@ -0,0 +1,165 @@
/** @odoo-module **/
import { click, editSelect, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let target;
let serverData;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
color: {
type: "selection",
selection: [
["red", "Red"],
["black", "Black"],
],
default: "red",
},
tz_offset: {
string: "tz_offset",
type: "char",
},
},
records: [
{ id: 1, color: "red", tz_offset: 0 },
{ id: 2, color: "red", tz_offset: 0 },
{ id: 3, color: "red", tz_offset: 0 },
],
},
},
};
setupViewRegistries();
});
QUnit.module("TimezoneMismatchField");
QUnit.test("widget timezone_mismatch in a list view", async function (assert) {
assert.expect(5);
serverData.models.partner.onchanges = {
color: function (r) {
r.tz_offset = "+4800"; // make sure we have a mismatch
},
};
await makeView({
type: "list",
resModel: "partner",
serverData,
resId: 1,
arch: /*xml*/ `
<tree string="Colors" editable="top">
<field name="tz_offset" invisible="True"/>
<field name="color" widget="timezone_mismatch" />
</tree>
`,
});
assert.containsN(target, "td:contains(Red)", 3, "should have 3 rows with correct value");
await click(
target
.querySelectorAll(".o_data_row")[0]
.querySelector("td:not(.o_list_record_selector)")
);
assert.containsOnce(
target,
".o_field_widget[name=color] select",
"td should have a child 'select'"
);
const td = target.querySelector("tbody tr.o_selected_row td:not(.o_list_record_selector)");
assert.strictEqual(
td.querySelector(".o_field_widget[name=color] select").parentElement.childElementCount,
1,
"select tag should be only child of td"
);
await editSelect(td, "select", '"black"');
assert.containsOnce(td, ".o_tz_warning", "Should display icon alert");
assert.ok(
td
.querySelector("select option:checked")
.textContent.match(/Black\s+\([0-9]+\/[0-9]+\/[0-9]+ [0-9]+:[0-9]+:[0-9]+\)/),
"Should display the datetime in the selected timezone"
);
});
QUnit.test("widget timezone_mismatch in a form view", async function (assert) {
assert.expect(2);
serverData.models.partner.fields.tz = {
type: "selection",
selection: [
["Europe/Brussels", "Europe/Brussels"],
["America/Los_Angeles", "America/Los_Angeles"],
],
};
serverData.models.partner.records[0].tz = false;
serverData.models.partner.records[0].tz_offset = "+4800";
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: /*xml*/ `
<form>
<field name="tz_offset" invisible="True"/>
<field name="tz" widget="timezone_mismatch" />
</form>
`,
});
assert.containsOnce(target, 'div[name="tz"] select');
assert.containsOnce(target, ".o_tz_warning", "warning class should be there.");
});
QUnit.test(
"widget timezone_mismatch in a form view edit mode with mismatch",
async function (assert) {
assert.expect(3);
serverData.models.partner.fields.tz = {
type: "selection",
selection: [
["Europe/Brussels", "Europe/Brussels"],
["America/Los_Angeles", "America/Los_Angeles"],
],
};
serverData.models.partner.records[0].tz = "America/Los_Angeles";
serverData.models.partner.records[0].tz_offset = "+4800";
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: /*xml*/ `
<form>
<field name="tz_offset" invisible="True"/>
<field name="tz" widget="timezone_mismatch" options="{'tz_offset_field': 'tz_offset'}"/>
</form>
`,
});
assert.containsN(
target,
'div[name="tz"] select option',
3,
"The select element should have 3 children"
);
assert.containsOnce(target, ".o_tz_warning", "timezone mismatch is present");
assert.notOk(
target.querySelector(".o_tz_warning").children.length,
"The mismatch element should not have children"
);
}
);
});

View file

@ -0,0 +1,340 @@
/** @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(() => {
serverData = {
models: {
partner: {
fields: {
foo: {
string: "Foo",
type: "char",
default: "My little Foo Value",
trim: true,
},
},
records: [
{
foo: "yop",
},
{
foo: "blip",
},
],
onchanges: {},
},
},
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("UrlField");
QUnit.test("UrlField in form view", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="url"/>
</group>
</sheet>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
'.o_field_widget input[type="text"]',
"should have an input for the url field"
);
assert.strictEqual(
target.querySelector('.o_field_widget input[type="text"]').value,
"yop",
"input should contain field value"
);
const webLink = target.querySelector(".o_field_url a");
assert.containsOnce(
target,
webLink,
"should have rendered the url button as a link with correct classes"
);
assert.hasAttrValue(webLink, "href", "http://yop", "should have proper href");
await editInput(target, ".o_field_widget input[type='text']", "limbo");
// save
const editedElement = ".o_field_widget input[type='text']";
assert.containsOnce(target, editedElement, "should still have an input for the url field");
assert.containsOnce(
target,
editedElement,
"should still have a anchor with correct classes"
);
assert.strictEqual(
target.querySelector(editedElement).value,
"limbo",
"has the proper value"
);
});
QUnit.test("UrlField in form view (readonly)", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="url" readonly="1"/>
</group>
</sheet>
</form>`,
resId: 1,
});
const matchingEl = target.querySelector("a.o_field_widget.o_form_uri");
assert.containsOnce(target, matchingEl, "should have a anchor with correct classes");
assert.hasAttrValue(matchingEl, "href", "http://yop", "should have proper href link");
assert.hasAttrValue(
matchingEl,
"target",
"_blank",
"should have target attribute set to _blank"
);
assert.strictEqual(matchingEl.textContent, "yop", "the value should be displayed properly");
});
QUnit.test("UrlField takes text from proper attribute", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: '<form><field name="foo" widget="url" text="kebeclibre" readonly="1"/></form>',
resId: 1,
});
assert.strictEqual(
target.querySelector('.o_field_widget[name="foo"] a').textContent,
"kebeclibre",
"url text should come from the text attribute"
);
});
QUnit.test("UrlField: href attribute and website_path option", async function (assert) {
serverData.models.partner.fields.url1 = {
string: "Url 1",
type: "char",
default: "www.url1.com",
};
serverData.models.partner.fields.url2 = {
string: "Url 2",
type: "char",
default: "www.url2.com",
};
serverData.models.partner.fields.url3 = {
string: "Url 3",
type: "char",
default: "http://www.url3.com",
};
serverData.models.partner.fields.url4 = {
string: "Url 4",
type: "char",
default: "https://url4.com",
};
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="url1" widget="url" readonly="1"/>
<field name="url2" widget="url" readonly="1" options="{'website_path': True}"/>
<field name="url3" widget="url" readonly="1"/>
<field name="url4" widget="url" readonly="1"/>
</form>`,
resId: 1,
});
assert.strictEqual(
target.querySelector('.o_field_widget[name="url1"] a').getAttribute("href"),
"http://www.url1.com"
);
assert.strictEqual(
target.querySelector('.o_field_widget[name="url2"] a').getAttribute("href"),
"www.url2.com"
);
assert.strictEqual(
target.querySelector('.o_field_widget[name="url3"] a').getAttribute("href"),
"http://www.url3.com"
);
assert.strictEqual(
target.querySelector('.o_field_widget[name="url4"] a').getAttribute("href"),
"https://url4.com"
);
});
QUnit.test("UrlField in editable list view", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "partner",
arch: '<tree editable="bottom"><field name="foo" widget="url"/></tree>',
});
assert.strictEqual(
target.querySelectorAll("tbody td:not(.o_list_record_selector) a").length,
2,
"should have 2 cells with a link"
);
assert.containsN(
target,
".o_field_url.o_field_widget[name='foo'] a",
2,
"should have 2 anchors with correct classes"
);
assert.hasAttrValue(
target.querySelector(".o_field_widget[name='foo'] a"),
"href",
"http://yop",
"should have proper href link"
);
assert.strictEqual(
target.querySelector("tbody td:not(.o_list_record_selector)").textContent,
"yop",
"value should be displayed properly as text"
);
// 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");
assert.strictEqual(
cell.querySelector("input").value,
"yop",
"should have the correct value in internal input"
);
await editInput(cell, "input", "brolo");
// save
await click(target.querySelector(".o_list_button_save"));
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
assert.doesNotHaveClass(
cell.parentElement,
"o_selected_row",
"should not be in edit mode anymore"
);
const resultEl = target.querySelector(".o_field_widget[name='foo'] a");
assert.containsN(
target,
".o_field_widget[name='foo'] a",
2,
"should still have anchors with correct classes"
);
assert.hasAttrValue(resultEl, "href", "http://brolo", "should have proper new href link");
assert.strictEqual(resultEl.textContent, "brolo", "value should be properly updated");
});
QUnit.test("UrlField with falsy value", async function (assert) {
serverData.models.partner.records[0].foo = false;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: '<form><field name="foo" widget="url"/></form>',
resId: 1,
});
assert.containsOnce(target, ".o_field_widget[name=foo] input");
assert.strictEqual(target.querySelector("[name=foo] input").value, "");
});
QUnit.test("UrlField: url old content is cleaned on render edit", async function (assert) {
serverData.models.partner.fields.foo2 = { string: "Foo2", type: "char", default: "foo2" };
serverData.models.partner.onchanges.foo2 = function (record) {
record.foo = record.foo2;
};
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="url" attrs="{'readonly': True}" />
<field name="foo2" />
</group>
</sheet>
</form>`,
resId: 1,
});
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo]").textContent,
"yop",
"the starting value should be displayed properly"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo2] input").value,
"foo2",
"input should contain field value in edit mode"
);
await editInput(target, ".o_field_widget[name=foo2] input", "bonjour");
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo]").textContent,
"bonjour",
"Url widget should show the new value and not " +
target.querySelector(".o_field_widget[name=foo]").textContent
);
});
QUnit.test("url 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" widget="url" placeholder="Placeholder"/>
</group>
</sheet>
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").placeholder,
"Placeholder"
);
});
QUnit.test("url field with non falsy, but non url value", async function (assert) {
serverData.models.partner.fields.foo.default = "odoo://hello";
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `<form><field name="foo" widget="url"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] a").getAttribute("href"),
"http://odoo://hello"
);
});
});