vanilla 17.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:47:08 +02:00
parent d72e748793
commit a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions

View file

@ -1,10 +1,17 @@
/** @odoo-module **/
/* global ace */
import { registry } from "@web/core/registry";
import { getFixture, triggerEvents } from "@web/../tests/helpers/utils";
import {
click,
clickSave,
editInput,
getFixture,
nextTick,
triggerEvent,
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;
@ -33,7 +40,6 @@ QUnit.module("Fields", (hooks) => {
};
setupViewRegistries();
registry.category("services").add("cookie", fakeCookieService);
});
QUnit.module("AceEditorField");
@ -46,7 +52,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="foo" widget="ace" />
<field name="foo" widget="code" />
</form>`,
});
@ -57,7 +63,96 @@ QUnit.module("Fields", (hooks) => {
"should have rendered something with ace editor"
);
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
assert.ok(target.querySelector(".o_field_code").textContent.includes("yop"));
});
QUnit.test("AceEditorField mark as dirty as soon at onchange", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="foo" widget="code" />
</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_code").textContent.includes("yop"));
// edit the foo field
const aceEditor = target.querySelector(".ace_editor");
ace.edit(aceEditor).setValue("blip");
await nextTick();
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.doesNotHaveClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible"
);
// revert edition
ace.edit(aceEditor).setValue("yop");
await nextTick();
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.hasClass(target.querySelector(".o_form_status_indicator_buttons"), "invisible");
});
QUnit.test("AceEditorField on html fields works", async function (assert) {
assert.expect(8);
serverData.models.partner.fields.htmlField = {
string: "HTML Field",
type: "html",
};
serverData.models.partner.records.push({
id: 3,
htmlField: "<p>My little HTML Test</p>",
});
serverData.models.partner.onchanges = { htmlField: function () {} };
await makeView({
type: "form",
resModel: "partner",
resId: 3,
serverData,
arch: `
<form>
<field name="foo"/>
<field name="htmlField" widget="code" />
</form>`,
mockRPC(route, args) {
if (args.method) {
assert.step(args.method);
if (args.method === "web_save") {
assert.deepEqual(args.args[1], { foo: "DEF" });
}
if (args.method === "onchange") {
throw new Error("Should not call onchange, htmlField wasn't changed");
}
}
},
});
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_code").textContent.includes("My little HTML Test")
);
// Modify foo and save
await editInput(target, ".o_field_widget[name=foo] textarea", "DEF");
await clickSave(target);
assert.verifySteps(["get_views", "web_read", "web_save"]);
});
QUnit.test("AceEditorField doesn't crash when editing", async (assert) => {
@ -69,7 +164,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="display_name" />
<field name="foo" widget="ace" />
<field name="foo" widget="code" />
</form>`,
});
@ -86,15 +181,17 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: /* xml */ `
<form>
<field name="foo" widget="ace" />
<field name="foo" widget="code" />
</form>`,
});
assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop"));
assert.ok(target.querySelector(".o_field_code").textContent.includes("yop"));
await pagerNext(target);
await nextTick();
await nextTick();
assert.ok(target.querySelector(".o_field_ace").textContent.includes("blip"));
assert.ok(target.querySelector(".o_field_code").textContent.includes("blip"));
});
QUnit.test(
@ -120,9 +217,74 @@ QUnit.module("Fields", (hooks) => {
},
});
assert.verifySteps(["get_views: []", 'read: [[1],["foo","display_name"]]']);
assert.verifySteps(["get_views: []", "web_read: [[1]]"]);
await pagerNext(target);
assert.verifySteps(['read: [[2],["foo","display_name"]]']);
assert.verifySteps(["web_read: [[2]]"]);
}
);
QUnit.test("AceEditorField only trigger onchanges when blurred", async (assert) => {
serverData.models.partner.onchanges = {
foo: (obj) => {},
};
serverData.models.partner.records.forEach((rec) => {
rec.foo = false;
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
resIds: [1, 2],
serverData,
arch: `<form>
<field name="display_name" />
<field name="foo" widget="code" />
</form>`,
mockRPC(route, args) {
if (args.method) {
assert.step(`${args.method}: ${JSON.stringify(args.args)}`);
}
},
});
assert.verifySteps(["get_views: []", "web_read: [[1]]"]);
const textArea = target.querySelector(".ace_editor textarea");
await click(textArea);
textArea.focus();
textArea.value = "a";
await triggerEvent(textArea, null, "input", {});
await triggerEvents(textArea, null, ["blur"]);
assert.verifySteps(['onchange: [[1],{"foo":"a"},["foo"],{"display_name":{},"foo":{}}]']);
await click(target, ".o_form_button_save");
assert.verifySteps(['web_save: [[1],{"foo":"a"}]']);
});
QUnit.test("Save and Discard buttons will become invisible after saving", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="display_name" />
<field name="foo" widget="code" />
</form>`,
});
const textArea = target.querySelector(".ace_editor textarea");
await click(textArea);
textArea.focus();
textArea.value = "a";
await triggerEvent(textArea, null, "input", {});
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.doesNotHaveClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible"
);
await click(target, ".o_form_button_save");
assert.containsOnce(target, ".o_form_status_indicator_buttons");
assert.hasClass(target.querySelector(".o_form_status_indicator_buttons"), "invisible");
});
});

View file

@ -91,7 +91,7 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_form_button_save");
var newRecord = _.last(serverData.models.partner.records);
var newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value");
});
@ -122,7 +122,7 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelector(".o_form_button_save"));
var newRecord = _.last(serverData.models.partner.records);
var newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(
newRecord.color,
"black",
@ -151,12 +151,18 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"BadgeSelectionField widget on a selection unchecking selected value",
async function (assert) {
async (assert) => {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: '<form><field name="color" widget="selection_badge"/></form>',
mockRPC(_, { method, model, args }) {
if (method === "web_save" && model === "partner") {
assert.step("web_save");
assert.deepEqual(args[1], { color: false });
}
},
});
assert.containsOnce(
@ -165,18 +171,20 @@ QUnit.module("Fields", (hooks) => {
"should have rendered outer div"
);
assert.containsN(target, "span.o_selection_badge", 2, "should have 2 possible choices");
assert.containsN(target, "span.o_selection_badge.active", 1, "one is active");
assert.strictEqual(
target.querySelector("span.o_selection_badge").textContent,
target.querySelector("span.o_selection_badge.active").textContent,
"Red",
"one of them should be Red"
"the active one should be Red"
);
// click again on red option
await click(target.querySelector("span.o_selection_badge.active"));
// click again on red option and save to update the server data
await click(target, "span.o_selection_badge.active");
assert.verifySteps([]);
await click(target, ".o_form_button_save");
assert.verifySteps(["web_save"], "should have created a new record");
await click(target.querySelector(".o_form_button_save"));
var newRecord = _.last(serverData.models.partner.records);
const newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(
newRecord.color,
false,
@ -184,4 +192,49 @@ QUnit.module("Fields", (hooks) => {
);
}
);
QUnit.test(
"BadgeSelectionField widget on a selection unchecking selected value (required field)",
async (assert) => {
serverData.models.partner.fields.color.required = true;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: '<form><field name="color" widget="selection_badge"/></form>',
mockRPC(_, { method, model, args }) {
if (method === "web_save" && model === "partner") {
assert.step("web_save");
assert.deepEqual(args[1], { color: "red" });
}
},
});
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.containsN(target, "span.o_selection_badge.active", 1, "one is active");
assert.strictEqual(
target.querySelector("span.o_selection_badge.active").textContent,
"Red",
"the active one should be Red"
);
// click again on red option and save to update the server data
await click(target, "span.o_selection_badge.active");
assert.verifySteps([]);
await click(target, ".o_form_button_save");
assert.verifySteps(["web_save"], "should have created a new record");
const newRecord = serverData.models.partner.records.at(-1);
assert.strictEqual(
newRecord.color,
"red",
"the new value should be red"
);
}
);
});

View file

@ -1,6 +1,7 @@
/** @odoo-module **/
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { makeServerError } from "@web/../tests/helpers/mock_server";
import { makeMockXHR } from "@web/../tests/helpers/mock_services";
import {
click,
@ -12,13 +13,16 @@ import {
} 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 { errorService } from "@web/core/errors/error_service";
import { registry } from "@web/core/registry";
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";
const serviceRegistry = registry.category("services");
let serverData;
let target;
@ -94,13 +98,7 @@ QUnit.module("Fields", (hooks) => {
}
const MockXHR = makeMockXHR("", send);
patchWithCleanup(
browser,
{
XMLHttpRequest: MockXHR,
},
{ pure: true }
);
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
await makeView({
serverData,
@ -167,13 +165,7 @@ QUnit.module("Fields", (hooks) => {
}
const MockXHR = makeMockXHR("", send);
patchWithCleanup(
browser,
{
XMLHttpRequest: MockXHR,
},
{ pure: true }
);
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
await makeView({
serverData,
@ -308,6 +300,36 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("icons are displayed exactly once", async (assert) => {
assert.expect(3);
patchWithCleanup(odoo, { debug: true });
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: /* xml */ `
<form>
<field name="document" filename="foo"/>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_field_binary .o_select_file_button",
"only one select file icon should be visible"
);
assert.containsOnce(
target,
".o_field_binary .o_download_file_button",
"only one download file icon should be visible"
);
assert.containsOnce(
target,
".o_field_binary .o_clear_file_button",
"only one clear file icon should be visible"
);
});
QUnit.test(
"binary fields input value is empty when clearing after uploading",
async function (assert) {
@ -380,13 +402,7 @@ QUnit.module("Fields", (hooks) => {
}
const MockXHR = makeMockXHR("", download);
patchWithCleanup(
browser,
{
XMLHttpRequest: MockXHR,
},
{ pure: true }
);
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
serverData.models.partner.onchanges = {
product_id: function (obj) {
@ -408,7 +424,10 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
});
await click(target, ".o_form_button_create");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .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")
@ -429,7 +448,7 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("Binary field in list view", async function (assert) {
QUnit.test("BinaryField in list view (formatter)", async function (assert) {
serverData.models.partner.records[0].document = BINARY_FILE;
await makeView({
@ -438,7 +457,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<tree>
<field name="document" filename="yooo"/>
<field name="document"/>
</tree>`,
resId: 1,
});
@ -449,7 +468,28 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Binary field for new record has no download button", async function (assert) {
QUnit.test("BinaryField in list view with filename", async function (assert) {
serverData.models.partner.records[0].document = BINARY_FILE;
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree>
<field name="document" filename="foo" widget="binary"/>
<field name="foo"/>
</tree>`,
resId: 1,
});
assert.strictEqual(
target.querySelector(".o_data_row .o_data_cell").textContent,
"coucou.txt"
);
});
QUnit.test("BinaryField for new record has no download button", async function (assert) {
serverData.models.partner.fields.document.default = BINARY_FILE;
await makeView({
serverData,
@ -466,8 +506,10 @@ QUnit.module("Fields", (hooks) => {
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");
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,
@ -528,14 +570,13 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test('isUploading state should be set to false after upload', async function(assert) {
assert.expect(1);
QUnit.test("isUploading state should be set to false after upload", async function (assert) {
serviceRegistry.add("error", errorService);
serverData.models.partner.onchanges = {
document: function (obj) {
if (obj.document) {
const error = new RPCError();
error.exceptionName = "odoo.exceptions.ValidationError";
throw error;
throw makeServerError({ type: "ValidationError" });
}
},
};
@ -552,16 +593,19 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_binary .o_input_file", file);
assert.equal(
target.querySelector(".o_select_file_button").innerText,
"UPLOAD YOUR FILE",
"Upload your file",
"displayed value should be upload your file"
);
assert.containsOnce(target, ".o_error_dialog");
});
QUnit.test("doesn't crash if value is not a string", async (assert) => {
serverData.models.partner.records = [{
id: 1,
document: {},
}]
serverData.models.partner.records = [
{
id: 1,
document: {},
},
];
await makeView({
type: "form",
@ -573,9 +617,6 @@ QUnit.module("Fields", (hooks) => {
<field name="document"/>
</form>`,
});
assert.equal(
target.querySelector(".o_field_binary input").value,
""
);
})
assert.equal(target.querySelector(".o_field_binary input").value, "");
});
});

View file

@ -56,7 +56,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
@ -69,11 +69,94 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_kanban_record .o_field_widget .o_favorite > a").textContent,
" Add to Favorites",
"Add to Favorites",
'the label should say "Add to Favorites"'
);
});
QUnit.test("FavoriteField saves changes by default", 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>`,
mockRPC(route, args) {
if (args.method === "web_save" && args.model === "partner") {
assert.step("save");
assert.deepEqual(args.args, [[1], { bar: false }]);
}
},
domain: [["id", "=", 1]],
});
// 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"'
);
assert.verifySteps(["save"]);
});
QUnit.test(
"FavoriteField does not save if autosave option is set to false",
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" options="{'autosave': False}"/>
</div>
</t>
</templates>
</kanban>`,
mockRPC(route, args) {
if (args.method === "web_save" && args.model === "partner") {
assert.step("save");
}
},
domain: [["id", "=", 1]],
});
// 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"'
);
assert.verifySteps([]);
}
);
QUnit.test("FavoriteField in form view", async function (assert) {
await makeView({
type: "form",
@ -97,7 +180,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
@ -110,7 +193,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Add to Favorites",
"Add to Favorites",
'the label should say "Add to Favorites"'
);
@ -121,7 +204,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Add to Favorites",
"Add to Favorites",
'the label should say "Add to Favorites"'
);
@ -134,7 +217,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
@ -147,7 +230,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(".o_field_widget .o_favorite > a").textContent,
" Remove from Favorites",
"Remove from Favorites",
'the label should say "Remove from Favorites"'
);
});
@ -159,7 +242,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<tree editable="bottom">
<field name="bar" widget="boolean_favorite" nolabel="1" />
<field name="bar" widget="boolean_favorite" nolabel="1" options="{'autosave': False}"/>
</tree>`,
});

View file

@ -190,9 +190,10 @@ QUnit.module("Fields", (hooks) => {
"the row is now selected, in edition"
);
assert.ok(
!cell.querySelector(".o-checkbox input:checked").disabled,
"input should now be enabled"
!cell.querySelector(".o-checkbox input:not(:checked)").disabled,
"input should now be enabled and unchecked"
);
await click(cell, ".o-checkbox");
await click(cell);
assert.notOk(
cell.querySelector(".o-checkbox input:checked").disabled,
@ -220,10 +221,9 @@ QUnit.module("Fields", (hooks) => {
"should now have only 3 checked input"
);
// Re-Edit the line and fake-check the checkbox
// Fake-check the checkbox
await click(cell);
await click(cell, ".o-checkbox");
await click(cell, ".o-checkbox");
// Save
await clickSave(target);
@ -273,4 +273,34 @@ QUnit.module("Fields", (hooks) => {
"checkbox should still be disabled"
);
});
QUnit.test("onchange return value before toggle checkbox", async function (assert) {
serverData.models.partner.onchanges = {
bar(obj) {
obj.bar = true;
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form><field name="bar"/></form>`,
});
assert.containsOnce(
target,
".o_field_boolean input:checked",
"checkbox should still be checked"
);
await click(target, ".o_field_boolean .o-checkbox");
await nextTick();
assert.containsOnce(
target,
".o_field_boolean input:checked",
"checkbox should still be checked"
);
});
});

View file

@ -0,0 +1,83 @@
/** @odoo-module **/
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { click } from "../../helpers/utils";
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 },
barOff: {
string: "Bar Off",
type: "boolean",
default: true,
searchable: true,
},
},
records: [{ id: 1, bar: true, barOff: false }],
},
},
};
setupViewRegistries();
});
QUnit.module("BooleanIconField");
QUnit.test("boolean_icon field in form view", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<label for="bar" string="Bar" />
<field name="bar" widget="boolean_icon" options="{'icon': 'fa-recycle'}" />
<field name="barOff" widget="boolean_icon" options="{'icon': 'fa-trash'}" />
</form>`,
});
assert.containsN(target, ".o_field_boolean_icon button", 2, "icon buttons are visible");
assert.strictEqual(
target.querySelector("[name='bar'] button").dataset.tooltip,
"Bar",
"first button has the label as tooltip"
);
assert.hasClass(
target.querySelector("[name='bar'] button"),
"btn-primary",
"active boolean button has the right class"
);
assert.hasClass(
target.querySelector("[name='bar'] button"),
"fa-recycle",
"first button has the right icon"
);
assert.hasClass(
target.querySelector("[name='barOff'] button"),
"btn-outline-secondary",
"inactive boolean button has the right class"
);
assert.hasClass(
target.querySelector("[name='barOff'] button"),
"fa-trash",
"second button has the right icon"
);
await click(target.querySelector("[name='bar'] button"));
assert.hasClass(
target.querySelector("[name='bar'] button"),
"btn-outline-secondary",
"boolean button is now inactive"
);
});
});

View file

@ -249,13 +249,13 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
await click(target, ".o_field_widget[name='bar'] input");
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
QUnit.test("BooleanToggleField - autosave option set to false", async function (assert) {
@ -269,8 +269,8 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});

View file

@ -190,7 +190,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { args, method }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(args[1].foo, false, "the foo value should be false");
}
},
@ -407,60 +407,63 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("translation dialog should close if field is not there anymore", async function (assert) {
// In this test, we simulate the case where the field is removed from the view
// this can happend for example if the user click the back button of the browser.
serverData.models.partner.fields.foo.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: `
QUnit.test(
"translation dialog should close if field is not there anymore",
async function (assert) {
// In this test, we simulate the case where the field is removed from the view
// this can happend for example if the user click the back button of the browser.
serverData.models.partner.fields.foo.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>
<sheet>
<group>
<field name="int_field" />
<field name="foo" attrs="{'invisible': [('int_field', '==', 9)]}"/>
<field name="foo" invisible="int_field == 9"/>
</group>
</sheet>
</form>`,
mockRPC(route, { args, method, model }) {
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([
["en_US", "English"],
["fr_BE", "French (Belgium)"],
["es_ES", "Spanish"],
]);
}
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" },
{ lang: "es_ES", source: "yop", value: "yop español" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
mockRPC(route, { args, method, model }) {
if (route === "/web/dataset/call_kw/res.lang/get_installed") {
return Promise.resolve([
["en_US", "English"],
["fr_BE", "French (Belgium)"],
["es_ES", "Spanish"],
]);
}
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" },
{ lang: "es_ES", source: "yop", value: "yop español" },
],
{ translation_type: "char", translation_show_source: false },
]);
}
},
});
assert.hasClass(target.querySelector("[name=foo] input"), "o_field_translate");
assert.hasClass(target.querySelector("[name=foo] input"), "o_field_translate");
await click(target, ".o_field_char .btn.o_field_translate");
assert.containsOnce(target, ".modal", "a translate modal should be visible");
await editInput(target, ".o_field_widget[name=int_field] input", "9");
await nextTick();
assert.containsNone(target, "[name=foo] input", "the field foo should be invisible");
assert.containsNone(target, ".modal", "a translate modal should not be visible");
});
await click(target, ".o_field_char .btn.o_field_translate");
assert.containsOnce(target, ".modal", "a translate modal should be visible");
await editInput(target, ".o_field_widget[name=int_field] input", "9");
await nextTick();
assert.containsNone(target, "[name=foo] input", "the field foo should be invisible");
assert.containsNone(target, ".modal", "a translate modal should not be visible");
}
);
QUnit.test("html field translatable", async function (assert) {
assert.expect(5);
@ -718,6 +721,77 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test(
"input field: change value before pending onchange returns (2)",
async function (assert) {
serverData.models.partner.onchanges = {
int_field(obj) {
if (obj.int_field === 7) {
obj.foo = "blabla";
} else {
obj.foo = "tralala";
}
},
};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<field name="int_field" />
<field name="foo" />
</sheet>
</form>`,
async mockRPC(route, { method }) {
if (method === "onchange") {
await def;
}
},
});
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"yop",
"should contain the correct value"
);
// trigger a deferred onchange
await editInput(target, ".o_field_widget[name='int_field'] input", "7");
// insert a value in input foo
target.querySelector(".o_field_widget[name=foo] input").value = "test";
await triggerEvent(target, ".o_field_widget[name=foo] input", "input");
// complete the onchange
def.resolve();
await nextTick();
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"test",
"The onchange value should not be applied because the input is in edition"
);
// apply the value of the input foo
await triggerEvent(target, ".o_field_widget[name=foo] input", "change");
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"test"
);
// trigger another onchange (not deferred)
await editInput(target, ".o_field_widget[name='int_field'] input", "10");
assert.strictEqual(
target.querySelector(".o_field_widget[name='foo'] input").value,
"tralala",
"the onchange value should be applied because the input is not in edition"
);
}
);
QUnit.test(
"input field: change value before pending onchange returns (with fieldDebounce)",
async function (assert) {
@ -792,6 +866,30 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("onchange return value before editing input", async function (assert) {
serverData.models.partner.onchanges = {
foo(obj) {
obj.foo = "yop";
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="foo" />
</form>`,
});
assert.strictEqual(target.querySelector("[name='foo'] input").value, "yop");
await editInput(target, "[name='foo'] input", "tralala");
assert.strictEqual(target.querySelector("[name='foo'] input").value, "yop");
});
QUnit.test(
"input field: change value before pending onchange renaming",
async function (assert) {
@ -1047,4 +1145,94 @@ QUnit.module("Fields", (hooks) => {
"Placeholder"
);
});
QUnit.test(
"char field: correct value is used to evaluate the modifiers",
async function (assert) {
serverData.models.partner.onchanges = {
foo: (obj) => {
if (obj.foo === "a") {
obj.display_name = false;
} else if (obj.foo === "b") {
obj.display_name = "";
}
},
};
serverData.models.partner.records[0].foo = false;
serverData.models.partner.records[0].display_name = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<field name="foo" />
<field name="display_name" invisible="'' == display_name"/>
</form>`,
});
assert.containsOnce(target, "[name='display_name'] input");
await editInput(target, "[name='foo'] input", "a");
assert.containsOnce(target, "[name='display_name'] input");
await editInput(target, "[name='foo'] input", "b");
assert.containsNone(target, "[name='display_name'] input");
}
);
QUnit.test(
"edit a char field should display the status indicator buttons without flickering",
async function (assert) {
serverData.models.partner.records[0].p = [2];
serverData.models.partner.onchanges = {
foo() {},
};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="foo"/>
</tree>
</field>
</form>`,
async mockRPC(route, { method }) {
if (method === "onchange") {
assert.step("onchange");
await def;
}
},
});
assert.containsOnce(
target,
".o_form_status_indicator_buttons.invisible",
"form view is not dirty"
);
await click(target, ".o_data_cell");
await editInput(target, "[name='foo'] input", "a");
assert.verifySteps(["onchange"]);
assert.containsOnce(
target,
".o_form_status_indicator_buttons:not(.invisible)",
"form view is dirty"
);
def.resolve();
await nextTick();
assert.containsOnce(
target,
".o_form_status_indicator_buttons:not(.invisible)",
"form view is dirty"
);
}
);
});

View file

@ -68,7 +68,7 @@ QUnit.module("Fields", (hooks) => {
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"}]',
'onchange [[1],{"hex_color":"#fefefe"},["hex_color"],{"hex_color":{},"display_name":{}}]',
]);
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
assert.strictEqual(
@ -113,31 +113,34 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
'.o_field_color input:disabled',
".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: `
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"
);
});
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 = {
@ -170,7 +173,7 @@ QUnit.module("Fields", (hooks) => {
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":""}]',
'onchange [[1],{"foo":"someValue"},["foo"],{"foo":{},"hex_color":{},"display_name":{}}]',
]);
assert.strictEqual(target.querySelector(".o_field_color input").value, "#fefefe");
assert.strictEqual(

View file

@ -224,7 +224,11 @@ QUnit.module("Fields", (hooks) => {
</tree>`,
domain: [["id", "<", 0]],
});
await click(target.querySelector(".o_list_button_add"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
)
);
const date_column_width = target
.querySelector('.o_list_table thead th[data-name="date_field"]')
.style.width.replace("px", "");

View file

@ -2,7 +2,13 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { click, getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import {
click,
getFixture,
editInput,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
const serviceRegistry = registry.category("services");
@ -73,8 +79,9 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("CopyClipboardField on unset field", async function (assert) {
QUnit.test("CopyClipboardField: show copy button even on empty field", async function (assert) {
serverData.models.partner.records[0].char_field = false;
serverData.models.partner.records[0].text_field = false;
await makeView({
serverData,
@ -85,26 +92,25 @@ QUnit.module("Fields", (hooks) => {
<sheet>
<group>
<field name="char_field" widget="CopyClipboardChar" />
<field name="text_field" widget="CopyClipboardText" />
</group>
</sheet>
</form>`,
resId: 1,
});
assert.containsNone(
assert.containsOnce(
target,
'.o_field_copy[name="char_field"] .o_clipboard_button',
"char_field (unset) should not contain a button"
'.o_field_CopyClipboardChar[name="char_field"] .o_clipboard_button'
);
assert.containsOnce(
target.querySelector(".o_field_widget[name=char_field]"),
"input",
"char_field (unset) should contain an input field"
target,
'.o_field_CopyClipboardText[name="text_field"] .o_clipboard_button'
);
});
QUnit.test(
"CopyClipboardField on readonly unset fields in create mode",
"CopyClipboardField: show copy button even on readonly empty field",
async function (assert) {
serverData.models.partner.fields.display_name.readonly = true;
@ -122,10 +128,9 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
assert.containsNone(
assert.containsOnce(
target,
'.o_field_copy[name="display_name"] .o_clipboard_button',
"the readonly unset field should not contain a button"
'.o_field_CopyClipboardChar[name="display_name"] .o_clipboard_button'
);
}
);
@ -190,42 +195,9 @@ QUnit.module("Fields", (hooks) => {
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,
},
});
QUnit.module("CopyClipboardButtonField");
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) {
QUnit.test("CopyClipboardButtonField in form view", async function (assert) {
patchWithCleanup(browser, {
navigator: {
clipboard: {
@ -267,4 +239,52 @@ Ho-ho-hoooo Merry Christmas`,
"yop",
]);
});
QUnit.test("CopyClipboardButtonField can be disabled", 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" disabled="1" widget="CopyClipboardButton"/>
<field name="char_field" disabled="char_field == 'yop'" widget="CopyClipboardButton"/>
<field name="char_field" widget="char"/>
</div>
</sheet>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_clipboard_button.o_btn_text_copy[disabled]",
"The inner button should be disabled."
);
assert.containsOnce(
target,
".o_clipboard_button.o_btn_char_copy[disabled]",
"The inner button should be disabled."
);
await editInput(target, ".o_input", "yip");
assert.containsNone(
target,
".o_clipboard_button.o_btn_char_copy[disabled]",
"The inner button should not be disabled."
);
});
});

View file

@ -1,6 +1,6 @@
/** @odoo-module **/
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { getPickerCell, zoomOut } from "@web/../tests/core/datetime/datetime_test_helpers";
import {
click,
clickCreate,
@ -16,8 +16,7 @@ import {
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";
import { localization } from "@web/core/l10n/localization";
let serverData;
let target;
@ -70,7 +69,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("DateField");
QUnit.test("DateField: toggle datepicker", async function (assert) {
QUnit.test("DateField: toggle datepicker", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -81,29 +80,21 @@ QUnit.module("Fields", (hooks) => {
<field name="date" />
</form>`,
});
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed initially"
);
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed initially");
await click(target, ".o_datepicker .o_datepicker_input");
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
// focus another field
await click(target, ".o_field_widget[name='foo'] input");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
target,
".o_datetime_picker",
"datepicker should close itself when the user clicks outside"
);
});
QUnit.test("DateField: toggle datepicker far in the future", async function (assert) {
QUnit.test("DateField: toggle datepicker far in the future", async (assert) => {
serverData.models.partner.records = [
{
id: 1,
@ -124,29 +115,21 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed initially"
);
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed initially");
await click(target, ".o_datepicker .o_datepicker_input");
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
// focus another field
await click(target, ".o_field_widget[name='foo'] input");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
target,
".o_datetime_picker",
"datepicker should close itself when the user clicks outside"
);
});
QUnit.test("date field is empty if no date is set", async function (assert) {
QUnit.test("date field is empty if no date is set", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -157,7 +140,7 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(
target,
".o_field_widget .o_datepicker_input",
".o_field_widget input",
"should have one input in the form view"
);
assert.strictEqual(
@ -167,47 +150,24 @@ QUnit.module("Fields", (hooks) => {
);
});
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>',
});
QUnit.test("DateField: set an invalid date when the field is already set", async (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");
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");
}
);
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) {
QUnit.test("DateField: set an invalid date when the field is not set yet", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -216,25 +176,42 @@ QUnit.module("Fields", (hooks) => {
arch: '<form><field name="date"/></form>',
});
await click(target, ".o_datepicker .o_datepicker_input");
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 (assert) => {
await makeView({
type: "form",
resModel: "partner",
resId: 4,
serverData,
arch: '<form><field name="date"/></form>',
});
await click(target, ".o_field_date 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/']");
await click(getPickerCell("22"));
// re-open datepicker
await click(target, ".o_datepicker .o_datepicker_input");
await click(target, ".o_field_date input");
assert.strictEqual(
document.body.querySelector(".day.active").textContent,
target.querySelector(".o_date_item_cell.o_selected").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) {
QUnit.test("DateField in form view (with positive time zone offset)", async (assert) => {
assert.expect(7);
patchTimeZone(120); // Should be ignored by date fields
@ -246,7 +223,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="date"/></form>',
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.strictEqual(
args[1].date,
"2017-02-22",
@ -257,39 +234,28 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date 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");
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
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"
target,
".o_date_item_cell.o_selected",
"datepicker should have a selected day"
);
// select 22 Feb 2017
await zoomOut();
await zoomOut();
await click(getPickerCell("2017"));
await click(getPickerCell("Feb"));
await click(getPickerCell("22"));
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/22/2017",
"the selected date should be displayed in the input"
);
@ -303,7 +269,7 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("DateField in form view (with negative time zone offset)", async function (assert) {
QUnit.test("DateField in form view (with negative time zone offset)", async (assert) => {
patchTimeZone(-120); // Should be ignored by date fields
await makeView({
@ -315,13 +281,13 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
});
QUnit.test("DateField dropdown disappears on scroll", async function (assert) {
QUnit.test("DateField dropdown doesn't disappear on scroll", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -335,22 +301,14 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, ".o_datepicker .o_datepicker_input");
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await triggerScroll(target, { top: 50 });
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed"
);
assert.containsOnce(target, ".o_datetime_picker", "datepicker should still be opened");
});
QUnit.test("DateField with label opens datepicker on click", async function (assert) {
QUnit.test("DateField with label opens datepicker on click", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -364,14 +322,10 @@ QUnit.module("Fields", (hooks) => {
});
await click(target.querySelector("label.o_form_label"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
});
QUnit.test("DateField with warn_future option", async function (assert) {
QUnit.test("DateField with warn_future option", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -379,42 +333,36 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="date" options="{ 'datepicker': { 'warn_future': true } }" />
<field name="date" options="{'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/']");
await click(target, ".o_field_date input");
await zoomOut();
await zoomOut();
await click(getPickerCell("2030"));
await click(getPickerCell("Dec"));
await click(getPickerCell("31"));
assert.containsOnce(
target,
".o_datepicker_warning",
".fa-exclamation-triangle",
"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
await editInput(target, ".o_field_widget[name='date'] input", "");
assert.containsNone(
target,
".o_datepicker_warning",
".fa-exclamation-triangle",
"the warning in the form view should be hidden"
);
});
QUnit.test(
"DateField with warn_future option: do not overwrite datepicker option",
async function (assert) {
async (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;
@ -428,7 +376,7 @@ QUnit.module("Fields", (hooks) => {
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 } }" />
<field name="date" options="{'warn_future': true}" />
</form>`,
});
@ -447,7 +395,7 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("DateField in editable list view", async function (assert) {
QUnit.test("DateField in editable list view", async (assert) => {
await makeView({
type: "list",
resModel: "partner",
@ -465,45 +413,33 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(
target,
"input.o_datepicker_input",
".o_field_date input",
"the view should have a date input for editable mode"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input"),
target.querySelector(".o_field_date input"),
document.activeElement,
"date input should have the focus"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input").value,
target.querySelector(".o_field_date 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"
);
await click(target, ".o_field_date input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await zoomOut();
await zoomOut();
await click(getPickerCell("2017"));
await click(getPickerCell("Feb"));
await click(getPickerCell("22"));
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/22/2017",
"the selected date should be displayed in the input"
);
@ -517,44 +453,41 @@ QUnit.module("Fields", (hooks) => {
);
});
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";
QUnit.test("multi edition of DateField in list view: clear date in input", async (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>',
});
await makeView({
serverData,
type: "list",
resModel: "partner",
arch: '<tree multi_edit="1"><field name="date"/></tree>',
});
const rows = target.querySelectorAll(".o_data_row");
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");
// 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");
await click(rows[0], ".o_data_cell");
assert.containsOnce(target, "input.o_datepicker_input");
await editInput(target, ".o_datepicker_input", "");
assert.containsOnce(target, ".o_field_date input");
await editInput(target, ".o_field_date input", "");
assert.containsOnce(document.body, ".modal");
await click(target, ".modal .modal-footer .btn-primary");
assert.containsOnce(target, ".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,
""
);
}
);
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) {
QUnit.test("DateField remove value", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -569,16 +502,16 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
const input = target.querySelector(".o_datepicker_input");
const input = target.querySelector(".o_field_date input");
input.value = "";
await triggerEvents(input, null, ["input", "change", "focusout"]);
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date input").value,
"",
"should have correctly removed the value"
);
@ -592,93 +525,32 @@ QUnit.module("Fields", (hooks) => {
);
});
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.",
QUnit.test("field date should select its content onclick when there is one", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: '<form><field name="date"/></form>',
});
registry.category("services").remove("localization");
registry
.category("services")
.add(
"localization",
makeFakeLocalizationService({ dateFormat: strftimeToLuxonFormat("%d-%m/%Y") })
);
patchWithCleanup(luxon.Settings, {
defaultLocale: "no",
const input = target.querySelector(".o_field_date input");
await click(input);
input.focus();
assert.containsOnce(target, ".o_datetime_picker");
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"
);
});
QUnit.test("DateField supports custom format", async (assert) => {
patchWithCleanup(localization, {
dateFormat: "dd-MM-yyyy",
});
await makeView({
@ -690,27 +562,60 @@ QUnit.module("Fields", (hooks) => {
});
const dateViewForm = target.querySelector(".o_field_date input").value;
await click(target, ".o_datepicker .o_datepicker_input");
await click(target, ".o_field_date input");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_date 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 click(getPickerCell("22"));
const dateEditForm = target.querySelector(".o_field_date 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) {
QUnit.test("DateField supports internationalization", async (assert) => {
patchWithCleanup(luxon.Settings, {
defaultLocale: "nb-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_field_date input");
assert.strictEqual(
target.querySelector(".o_field_date input").value,
dateViewForm,
"input date field should be the same as it was in the view form"
);
assert.strictEqual(
target.querySelector(".o_zoom_out strong").textContent,
"februar 2017",
"Norwegian locale should be correctly applied"
);
await click(getPickerCell("22"));
const dateEditForm = target.querySelector(".o_field_date 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"
);
});
QUnit.test("DateField: hit enter should update value", async (assert) => {
patchTimeZone(120);
await makeView({
@ -725,23 +630,23 @@ QUnit.module("Fields", (hooks) => {
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");
await triggerEvent(input, null, "keydown", { key: "Enter" });
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");
await triggerEvent(input, null, "keydown", { key: "Enter" });
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) {
QUnit.test("DateField: allow to use compute dates (+5d for instance)", async (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";
@ -755,20 +660,20 @@ QUnit.module("Fields", (hooks) => {
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");
await editInput(target, ".o_field_widget[name=date] 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");
await editInput(target, ".o_field_widget[name=date] 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");
await editInput(target, ".o_field_widget[name=date] input", "+5d");
assert.strictEqual(target.querySelector(".o_field_widget input").value, "02/20/2021");
});
});

View file

@ -1,17 +1,25 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import {
getPickerApplyButton,
getPickerCell,
getTimePickers,
zoomOut,
} from "@web/../tests/core/datetime/datetime_test_helpers";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import {
click,
clickSave,
editInput,
editSelect,
getFixture,
patchTimeZone,
patchWithCleanup,
triggerEvent,
triggerEvents,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
let serverData;
let target;
@ -55,7 +63,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("DatetimeField");
QUnit.test("DatetimeField in form view", async function (assert) {
QUnit.test("DatetimeField in form view", async (assert) => {
patchTimeZone(120);
await makeView({
@ -70,56 +78,32 @@ QUnit.module("Fields", (hooks) => {
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"
"the datetime should be correctly displayed"
);
// datepicker should not open on focus
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
assert.containsNone(target, ".o_datetime_picker");
await click(target, ".o_datepicker_input");
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
await click(target, ".o_field_datetime input");
assert.containsOnce(target, ".o_datetime_picker");
// 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]);
// select 22 April 2018 at 8:25
await zoomOut();
await zoomOut();
await click(getPickerCell("2018"));
await click(getPickerCell("Apr"));
await click(getPickerCell("22"));
const [hourSelect, minuteSelect] = getTimePickers().at(0);
await editSelect(hourSelect, null, "8");
await editSelect(minuteSelect, null, "25");
// Close the datepicker
await click(target, ".o_form_view_container");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed"
);
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
const newExpectedDateString = "04/22/2017 08:25:35";
const newExpectedDateString = "04/22/2018 08:25:00";
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
newExpectedDateString,
"the selected date should be displayed in the input"
);
@ -134,8 +118,8 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"DatetimeField does not trigger fieldChange before datetime completly picked",
async function (assert) {
"DatetimeField only triggers fieldChange when a day is picked and when an hour/minute is selected",
async (assert) => {
patchTimeZone(120);
serverData.models.partner.onchanges = {
@ -154,60 +138,38 @@ QUnit.module("Fields", (hooks) => {
},
});
await click(target, ".o_datepicker_input");
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
await click(target, ".o_field_datetime input");
// 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.containsOnce(target, ".o_datetime_picker");
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
// select 22 April 2018 at 8:25
await zoomOut();
await zoomOut();
await click(getPickerCell("2018"));
await click(getPickerCell("Apr"));
await click(getPickerCell("22"));
assert.verifySteps([]);
const [hourSelect, minuteSelect] = getTimePickers().at(0);
await editSelect(hourSelect, null, "8");
await editSelect(minuteSelect, null, "25");
assert.verifySteps([]);
// Close the datepicker
await click(target);
assert.containsNone(target, ".o_datetime_picker");
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
"04/22/2017 08:25:35"
target.querySelector(".o_field_datetime input").value,
"04/22/2018 08:25:00"
);
assert.verifySteps(["onchange"], "should have done only one onchange");
assert.verifySteps(["onchange"]);
}
);
QUnit.test("DatetimeField with datetime formatted without second", async function (assert) {
QUnit.test("DatetimeField with datetime formatted without second", async (assert) => {
patchTimeZone(0);
serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
@ -234,14 +196,13 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_field_datetime input").value,
expectedDateString,
"the datetime should be correctly displayed in readonly"
"the datetime should be correctly displayed"
);
await click(target, ".o_form_button_cancel");
assert.containsNone(document.body, ".modal", "there should not be a Warning dialog");
assert.containsNone(target, ".modal", "there should not be a Warning dialog");
});
QUnit.test("DatetimeField in editable list view", async function (assert) {
QUnit.test("DatetimeField in editable list view", async (assert) => {
patchTimeZone(120);
await makeView({
@ -256,69 +217,54 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
cell.textContent,
expectedDateString,
"the datetime should be correctly displayed in readonly"
"the datetime should be correctly displayed"
);
// switch to edit mode
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(
target,
"input.o_datepicker_input",
".o_field_datetime input",
"the view should have a date input for editable mode"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input"),
target.querySelector(".o_field_datetime input"),
document.activeElement,
"date input should have the focus"
);
assert.strictEqual(
target.querySelector("input.o_datepicker_input").value,
target.querySelector(".o_field_datetime 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");
assert.containsNone(target, ".o_datetime_picker");
// 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]);
await click(target, ".o_field_datetime input");
assert.containsNone(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be closed"
);
assert.containsOnce(target, ".o_datetime_picker");
const newExpectedDateString = "04/22/2017 08:25:35";
// select 22 April 2018 at 8:25
await zoomOut();
await zoomOut();
await click(getPickerCell("2018"));
await click(getPickerCell("Apr"));
await click(getPickerCell("22"));
const [hourSelect, minuteSelect] = getTimePickers().at(0);
await editSelect(hourSelect, null, "8");
await editSelect(minuteSelect, null, "25");
// Apply changes
await click(getPickerApplyButton());
assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
const newExpectedDateString = "04/22/2018 08:25:00";
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
newExpectedDateString,
"the selected datetime should be displayed in the input"
"the date should be updated in the input"
);
// save
@ -332,7 +278,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"multi edition of DatetimeField in list view: edit date in input",
async function (assert) {
async (assert) => {
await makeView({
serverData,
type: "list",
@ -348,10 +294,12 @@ QUnit.module("Fields", (hooks) => {
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(target, ".o_field_datetime input");
await editInput(target, ".o_field_datetime input", "10/02/2019 09:00:00");
assert.containsOnce(target, ".modal");
assert.containsOnce(document.body, ".modal");
await click(target.querySelector(".modal .modal-footer .btn-primary"));
assert.strictEqual(
@ -367,7 +315,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"multi edition of DatetimeField in list view: clear date in input",
async function (assert) {
async (assert) => {
serverData.models.partner.records[1].datetime = "2017-02-08 10:00:00";
await makeView({
@ -385,10 +333,12 @@ QUnit.module("Fields", (hooks) => {
await click(rows[0], ".o_data_cell");
assert.containsOnce(target, "input.o_datepicker_input");
await editInput(target, ".o_datepicker_input", "");
assert.containsOnce(target, ".o_field_datetime input");
await editInput(target, ".o_field_datetime input", "");
assert.containsOnce(target, ".modal");
assert.containsOnce(document.body, ".modal");
await click(target, ".modal .modal-footer .btn-primary");
assert.strictEqual(
@ -402,7 +352,7 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test("DatetimeField remove value", async function (assert) {
QUnit.test("DatetimeField remove value", async (assert) => {
assert.expect(4);
patchTimeZone(120);
@ -414,7 +364,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="datetime"/></form>',
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.strictEqual(
args[1].datetime,
false,
@ -425,16 +375,16 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
"02/08/2017 12:00:00",
"the date time should be correct in edit mode"
);
const input = target.querySelector(".o_datepicker_input");
const input = target.querySelector(".o_field_datetime input");
input.value = "";
await triggerEvents(input, null, ["input", "change", "focusout"]);
assert.strictEqual(
target.querySelector(".o_datepicker_input").value,
target.querySelector(".o_field_datetime input").value,
"",
"should have an empty input"
);
@ -449,8 +399,8 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"DatetimeField with date/datetime widget (with day change)",
async function (assert) {
"DatetimeField with date/datetime widget (with day change) does not care about widget",
async (assert) => {
patchTimeZone(-240);
serverData.models.partner.records[0].p = [2];
@ -484,16 +434,16 @@ QUnit.module("Fields", (hooks) => {
// 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",
target.querySelector(".modal .o_field_date[name='datetime'] input").value,
"02/07/2017 22:00:00",
"the datetime (date widget) should be correctly displayed in form view"
);
}
);
QUnit.test(
"DatetimeField with date/datetime widget (without day change)",
async function (assert) {
"DatetimeField with date/datetime widget (without day change) does not care about widget",
async (assert) => {
patchTimeZone(-240);
serverData.models.partner.records[0].p = [2];
@ -527,44 +477,14 @@ QUnit.module("Fields", (hooks) => {
// 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",
target.querySelector(".modal .o_field_date[name='datetime'] input").value,
"02/08/2017 06:00:00",
"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) {
QUnit.test("datetime field: hit enter should update value", async (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
@ -609,36 +529,7 @@ QUnit.module("Fields", (hooks) => {
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) {
QUnit.test("DateTimeField with label opens datepicker on click", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@ -652,10 +543,66 @@ QUnit.module("Fields", (hooks) => {
});
await click(target.querySelector("label.o_form_label"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
});
QUnit.test("datetime field: use picker with arabic numbering system", async (assert) => {
patchWithCleanup(luxon.Settings, {
defaultLocale: "ar-001",
defaultNumberingSystem: "arab",
});
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: /* xml */ `
<form string="Partners">
<field name="datetime" />
</form>
`,
});
const getInput = () => target.querySelector("[name=datetime] input");
assert.strictEqual(getInput().value, "٠٢/٠٨/٢٠١٧ ١١:٠٠:٠٠");
await click(getInput());
await editSelect(getTimePickers()[0][1], null, 45);
assert.strictEqual(getInput().value, "٠٢/٠٨/٢٠١٧ ١١:٤٥:٠٠");
});
QUnit.test("list datetime with date widget test", async (assert) => {
await makeView({
type: "list",
resModel: "partner",
arch: /* xml */ `
<tree editable="bottom">
<field name="datetime" widget="datetime" options="{'show_time': false}"/>
<field name="datetime" widget="datetime"/>
</tree>`,
serverData,
});
const dates = target.querySelectorAll(".o_field_cell");
assert.strictEqual(
dates[0].textContent,
"02/08/2017",
"for datetime field only date should be visible with show_time as false and readonly"
);
assert.strictEqual(
dates[1].textContent,
"02/08/2017 11:00:00",
"for datetime field both date and time should be visible with show_time by default true"
);
await click(dates[0]);
assert.strictEqual(
target.querySelector(".o_field_datetime input").value,
"02/08/2017 11:00:00",
"for datetime field both date and time should be visible with show_time as false and edit"
);
});
});

View file

@ -9,14 +9,34 @@ import {
getFixture,
makeDeferred,
nextTick,
patchDate,
patchWithCleanup,
triggerEvent,
} from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers";
import * as dsHelpers from "@web/../tests/core/domain_selector_tests";
import { registry } from "@web/core/registry";
let serverData;
let target;
function replaceNotificationService(assert) {
registry.category("services").add(
"notification",
{
start() {
return {
add(message) {
assert.step(message);
},
};
},
},
{ force: true }
);
}
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
@ -89,19 +109,20 @@ QUnit.module("Fields", (hooks) => {
},
},
};
setupViewRegistries();
});
QUnit.module("DomainField");
QUnit.test(
"The domain editor should not crash the view when given a dynamic filter",
"The domain editor should not crash the view when given a dynamic filter (allow_expressions=False)",
async function (assert) {
// dynamic filters (containing variables, such as uid, parent or today)
// are not handled by the domain editor, but it shouldn't crash the view
// are handled by the domain editor
serverData.models.partner.records[0].foo = `[("int_field", "=", uid)]`;
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
@ -115,20 +136,51 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
target.querySelector(".o_edit_mode").textContent,
" This domain is not supported. Reset domain",
"The widget should not crash the view, but gracefully admit its failure."
dsHelpers.getCurrentValue(target),
"uid",
"The widget should show the dynamic filter."
);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain should not involve non-literals"]);
}
);
QUnit.test(
"The domain editor should not crash the view when given a dynamic filter (allow_expressions=True)",
async function (assert) {
// dynamic filters (containing variables, such as uid, parent or today)
// are handled by the domain editor
serverData.models.partner.records[0].foo = `[("int_field", "=", uid)]`;
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="foo" widget="domain" options="{'model': 'partner', 'allow_expressions':True}" />
<field name="int_field" invisible="1" />
</form>`,
});
assert.strictEqual(
dsHelpers.getCurrentValue(target),
"uid",
"The widget should show the dynamic filter."
);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain involves non-literals. Their evaluation might fail."]);
}
);
QUnit.test(
"The domain editor should not crash the view when given a dynamic filter ( datetime )",
async function (assert) {
// dynamic filters (containing variables, such as uid, parent or today)
// are not handled by the domain editor, but it shouldn't crash the view
serverData.models.partner.records[0].foo = `[("datetime", "=", context_today())]`;
serverData.models.partner.fields.datetime = { string: "A date", type: "datetime" };
serverData.models.partner.records[0].foo = `[("datetime", "=", context_today())]`;
await makeView({
type: "form",
@ -141,27 +193,20 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
// The input field should display that the date is invalid
assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime");
assert.equal(dsHelpers.getCurrentValue(target), "context_today()");
await dsHelpers.clearNotSupported(target);
// Change the date in the datepicker
await click(target, ".o_datepicker_input");
await click(target, ".o_datetime_input");
// Select a date in the datepicker
await click(
document.body.querySelector(
`.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]`
)
);
await click(getPickerCell("15"));
// Close the datepicker
await click(
document.body.querySelector(
`.bootstrap-datetimepicker-widget a[data-action="close"]`
)
);
await click(target);
await clickDiscard(target);
// Open the datepicker again
await click(target, ".o_datepicker_input");
assert.equal(dsHelpers.getCurrentValue(target), "context_today()");
}
);
@ -183,48 +228,32 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
// As the domain is empty, there should be a button to add the first
// domain part
assert.containsOnce(
target,
".o_domain_add_first_node_button",
"there should be a button to create first domain element"
);
// As the domain is empty, there should be a button to add a new rule
assert.containsOnce(target, dsHelpers.SELECTORS.addNewRule);
// Clicking on the button should add the [["id", "=", "1"]] domain, so
// there should be a field selector in the DOM
await click(target, ".o_domain_add_first_node_button");
assert.containsOnce(target, ".o_field_selector", "there should be a field selector");
await dsHelpers.addNewRule(target);
assert.containsOnce(target, ".o_model_field_selector", "there should be a field selector");
// Focusing the field selector input should open the field selector
// popover
await click(target, ".o_field_selector");
assert.containsOnce(
document.body,
".o_field_selector_popover",
"field selector popover should be visible"
);
assert.containsOnce(
document.body,
".o_field_selector_search input",
"field selector popover should contain a search input"
);
await click(target, ".o_model_field_selector");
assert.containsOnce(document.body, ".o_model_field_selector_popover");
assert.containsOnce(document.body, ".o_model_field_selector_popover_search input");
// The popover should contain the list of partner_type fields and so
// there should be the "Color index" field
assert.strictEqual(
document.body.querySelector(".o_field_selector_item").textContent,
"Color index",
"field selector popover should contain 'Color index' field"
document.body.querySelector(".o_model_field_selector_popover_item_name").textContent,
"Color index"
);
// Clicking on this field should close the popover, then changing the
// associated value should reveal one matched record
await click(document.body.querySelector(".o_field_selector_item"));
await click(document.body.querySelector(".o_model_field_selector_popover_item_name"));
const input = target.querySelector(".o_domain_leaf_value_input");
input.value = 2;
await triggerEvent(input, null, "change");
await dsHelpers.editValue(target, 2);
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2),
@ -235,10 +264,7 @@ QUnit.module("Fields", (hooks) => {
// Saving the form view should show a readonly domain containing the
// "color" field
await clickSave(target);
assert.ok(
target.querySelector(".o_field_domain").textContent.includes("Color index"),
"field selector readonly value should now contain 'Color index'"
);
assert.ok(target.querySelector(".o_field_domain").textContent.includes("Color index"));
});
QUnit.test("using binary field in domain widget", async function (assert) {
@ -260,9 +286,13 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, ".o_domain_add_first_node_button");
await click(target, ".o_field_selector");
await click(document.body.querySelector(".o_field_selector_item[data-name='image']"));
await dsHelpers.addNewRule(target);
await click(target, ".o_model_field_selector");
await click(
document.body.querySelector(
".o_model_field_selector_popover_item[data-name='image'] button"
)
);
});
QUnit.test("domain field is correctly reset on every view change", async function (assert) {
@ -290,15 +320,15 @@ QUnit.module("Fields", (hooks) => {
// selector to change this
assert.containsOnce(
target,
".o_field_domain .o_field_selector",
".o_field_domain .o_model_field_selector",
"there should be a field selector"
);
// Focusing its input should open the field selector popover
await click(target.querySelector(".o_field_selector"));
await click(target.querySelector(".o_model_field_selector"));
assert.containsOnce(
document.body,
".o_field_selector_popover",
".o_model_field_selector_popover",
"field selector popover should be visible"
);
@ -306,11 +336,11 @@ QUnit.module("Fields", (hooks) => {
// popover should contain the list of "product" fields
assert.containsOnce(
document.body,
".o_field_selector_item",
".o_model_field_selector_popover_item",
"field selector popover should contain only one field"
);
assert.strictEqual(
document.body.querySelector(".o_field_selector_item").textContent,
document.body.querySelector(".o_model_field_selector_popover_item").textContent,
"Product Name",
"field selector popover should contain 'Product Name' field"
);
@ -319,22 +349,22 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name='bar'] input", "partner_type");
// Refocusing the field selector input should open the popover again
await click(target.querySelector(".o_field_selector"));
await click(target.querySelector(".o_model_field_selector"));
assert.containsOnce(
document.body,
".o_field_selector_popover",
".o_model_field_selector_popover",
"field selector popover should be visible"
);
// Now the list of fields should be the ones of the "partner_type" model
assert.containsN(
document.body,
".o_field_selector_item",
".o_model_field_selector_popover_item",
2,
"field selector popover should contain two fields"
);
assert.strictEqual(
document.body.querySelector(".o_field_selector_item").textContent,
document.body.querySelector(".o_model_field_selector_popover_item").textContent,
"Color index",
"field selector popover should contain 'Color index' field"
);
@ -405,16 +435,8 @@ QUnit.module("Fields", (hooks) => {
}
},
});
assert.containsOnce(
target,
".o_field_widget[name='foo']:not(.o_field_empty)",
"there should be a domain field, not considered empty"
);
assert.containsNone(
target,
".o_field_widget[name='foo'] .text-warning",
"should not display that the domain is invalid"
);
assert.containsOnce(target, ".o_field_widget[name='foo']:not(.o_field_empty)");
assert.containsNone(target, ".o_field_widget[name='foo'] .text-warning");
});
QUnit.test("basic domain field: show the selection", async function (assert) {
@ -533,7 +555,7 @@ QUnit.module("Fields", (hooks) => {
"2 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '<', 40]]");
// the count should not be re-computed when editing with the textarea
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
@ -588,7 +610,7 @@ QUnit.module("Fields", (hooks) => {
throw new Error("should not save");
}
if (route === "/web/domain/validate") {
return JSON.stringify(domain) === "[[\"abc\",\"=\",1]]";
return JSON.stringify(domain) === '[["abc","=",1]]';
}
},
});
@ -601,7 +623,7 @@ QUnit.module("Fields", (hooks) => {
"2 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['abc']]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['abc', '=', 1]]");
// the count should not be re-computed when editing with the textarea
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
@ -609,6 +631,9 @@ QUnit.module("Fields", (hooks) => {
);
assert.verifySteps([]);
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['abc']]");
assert.verifySteps([]);
await clickSave(target);
assert.hasClass(
target.querySelector(".o_field_domain"),
@ -673,7 +698,7 @@ QUnit.module("Fields", (hooks) => {
"2 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '<', 40]]");
// the count should not be re-computed when editing with the textarea
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
@ -732,11 +757,7 @@ QUnit.module("Fields", (hooks) => {
patchWithCleanup(odoo, { debug: true });
let rawDomain = `
[
["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -365), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")]
]
`;
let rawDomain = `[("date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -365), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S"))]`;
serverData.models.partner.records[0].foo = rawDomain;
serverData.models.partner.fields.bar.type = "char";
serverData.models.partner.records[0].bar = "partner";
@ -745,7 +766,7 @@ QUnit.module("Fields", (hooks) => {
"partner,false,form": `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
<field name="foo" widget="domain" options="{'model': 'bar', 'allow_expressions':True}"/>
</form>`,
"partner,false,search": `<search />`,
};
@ -764,7 +785,7 @@ QUnit.module("Fields", (hooks) => {
const webClient = await createWebClient({
serverData,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(args[1].foo, rawDomain);
}
if (route === "/web/domain/validate") {
@ -774,21 +795,18 @@ QUnit.module("Fields", (hooks) => {
});
await doAction(webClient, 1);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
rawDomain = `
[
["date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -1), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S")]
]
`;
await editInput(target, ".o_domain_debug_input", rawDomain);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
rawDomain = `[("date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -1), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S"))]`;
await editInput(target, dsHelpers.SELECTORS.debugArea, rawDomain);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
await clickSave(target);
});
QUnit.test("domain field: edit through selector (dynamic content)", async function (assert) {
patchWithCleanup(odoo, { debug: true });
patchDate(2020, 8, 5, 0, 0, 0);
let rawDomain = `[("date", ">=", context_today())]`;
serverData.models.partner.records[0].foo = rawDomain;
@ -799,7 +817,7 @@ QUnit.module("Fields", (hooks) => {
"partner,false,form": `
<form>
<field name="bar"/>
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
<field name="foo" widget="domain" options="{'model': 'bar', 'allow_expressions':True}"/>
</form>`,
"partner,false,search": `<search />`,
};
@ -824,29 +842,39 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps(["/web/webclient/load_menus"]);
await doAction(webClient, 1);
assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]);
assert.verifySteps([
"/web/action/load",
"get_views",
"web_read",
"search_count",
"fields_get",
]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
await dsHelpers.clearNotSupported(target);
rawDomain = `[("date", ">=", "2020-09-05")]`;
assert.containsOnce(target, ".o_datetime_input", "there should be a datepicker");
assert.verifySteps(["search_count"]);
// Open and close the datepicker
await click(target, ".o_datepicker_input");
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
await click(target, ".o_datetime_input");
assert.containsOnce(target, ".o_datetime_picker");
await triggerEvent(window, null, "scroll");
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.containsOnce(target, ".o_datetime_picker");
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
assert.verifySteps([]);
// Manually input a date
rawDomain = `[("date", ">=", "2020-09-09")]`;
await editInput(target, ".o_datepicker_input", "09/09/2020");
await editInput(target, ".o_datetime_input", "09/09/2020");
assert.verifySteps(["search_count"]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
// Save
await clickSave(target);
assert.verifySteps(["write", "read", "search_count"]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.verifySteps(["web_save", "search_count"]);
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
});
QUnit.test("domain field without model", async function (assert) {
@ -874,13 +902,15 @@ QUnit.module("Fields", (hooks) => {
"should contain an error message saying the model is missing"
);
assert.verifySteps([]);
await editInput(target, ".o_field_widget[name=model_name] input", "test");
assert.notStrictEqual(
target.querySelector('.o_field_widget[name="display_name"]').innerText,
"Select a model to add a filter.",
"should not contain an error message anymore"
await editInput(target, ".o_field_widget[name=model_name] input", "partner");
assert.strictEqual(
target
.querySelector('.o_field_widget[name="display_name"] .o_field_domain_panel')
.innerText.toLowerCase(),
"5 record(s)"
);
assert.verifySteps(["test"]);
assert.verifySteps(["partner"]);
});
QUnit.test("domain field in kanban view", async function (assert) {
@ -936,20 +966,27 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="display_name" widget="domain" options="{'model': 'partner', 'in_dialog': True}"/>
</form>`,
mockRPC: (route) => {
if (route === "/web/domain/validate") {
return true;
}
},
});
assert.containsNone(target, ".o_domain_leaf");
assert.containsNone(target, dsHelpers.SELECTORS.condition);
assert.containsNone(target, ".modal");
await click(target, ".o_field_domain_dialog_button");
assert.containsOnce(target, ".modal");
await click(target, ".modal .o_domain_add_first_node_button");
await click(target, `.modal ${dsHelpers.SELECTORS.addNewRule}`);
await click(target, ".modal-footer .btn-primary");
assert.containsOnce(target, ".o_domain_leaf");
assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, "ID = 1");
assert.containsOnce(target, dsHelpers.SELECTORS.condition);
assert.strictEqual(dsHelpers.getConditionText(target), "ID = 1");
});
QUnit.test("invalid value in domain field with 'inDialog' options", async function (assert) {
serverData.models.partner.fields.display_name.default = "[]";
patchWithCleanup(odoo, {
debug: true,
});
await makeView({
type: "form",
resModel: "partner",
@ -958,33 +995,59 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="display_name" widget="domain" options="{'model': 'partner', 'in_dialog': True}"/>
</form>`,
mockRPC: (route, args) => {
if (args.method === "search_count") {
const domain = args.args[0];
if (domain.length && domain[0][0] === "id" && domain[0][2] === "01/01/2002") {
throw new Error("Invalid Domain");
}
}
},
});
assert.containsNone(target, ".o_domain_leaf");
assert.containsNone(target, dsHelpers.SELECTORS.condition);
assert.containsNone(target, ".modal");
assert.containsNone(target, ".o_field_domain .text-warning");
await click(target, ".o_field_domain_dialog_button");
assert.containsOnce(target, ".modal");
await click(target, ".modal .o_domain_add_first_node_button");
await editInput(target, ".o_domain_leaf_value_input", "01/01/2002");
await click(target, `.modal ${dsHelpers.SELECTORS.addNewRule}`);
await editInput(target, dsHelpers.SELECTORS.debugArea, "[(0, '=', expr)]");
await click(target, ".modal-footer .btn-primary");
assert.containsOnce(target, ".o_domain_leaf");
assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, 'ID = "01/01/2002"');
assert.strictEqual(
target.querySelector(".o_field_domain .text-warning").textContent.trim(),
"Invalid domain"
);
assert.containsOnce(target, ".modal", "the domain is invalid: the dialog is not closed");
});
QUnit.test(
"edit domain button is available even while loading records count",
async function (assert) {
serverData.models.partner.fields.display_name.default = "[]";
patchWithCleanup(odoo, {
debug: true,
});
const searchCountDeffered = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="display_name" widget="domain" options="{'model': 'partner', 'in_dialog': True}"/>
</form>`,
mockRPC: async (route) => {
if (route === "/web/dataset/call_kw/partner/search_count") {
await searchCountDeffered;
}
if (route === "/web/domain/validate") {
return true;
}
},
});
assert.containsNone(target, ".modal");
assert.containsOnce(target, ".o_field_domain_dialog_button");
await click(target, ".o_field_domain_dialog_button");
searchCountDeffered.resolve();
assert.containsOnce(target, ".modal");
await click(target, ".modal-footer .btn-primary");
assert.containsNone(target, ".modal");
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent,
"5 record(s) "
);
}
);
QUnit.test(
"quick check on save if domain has been edited via the debug input",
async function (assert) {
@ -1013,7 +1076,7 @@ QUnit.module("Fields", (hooks) => {
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
"0 record(s)"
);
await editInput(target, ".o_domain_debug_input", "[['id', '!=', False]]");
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '!=', False]]");
await click(target, "button.o_form_button_save");
assert.verifySteps(["/web/domain/validate"]);
assert.strictEqual(
@ -1022,4 +1085,226 @@ QUnit.module("Fields", (hooks) => {
);
}
);
QUnit.test("domain field can be foldable", async function (assert) {
serverData.models.partner.records[0].foo = "[]";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'foldable': true}" />
</group>
</sheet>
</form>`,
});
// As the domain is empty, the "Match all records" span should be visible
assert.strictEqual(
target.querySelector(".o_field_domain span").textContent,
"Match all records"
);
// Unfold the domain
await click(target, ".o_field_domain > div > div");
// There should be a button to add a new rule
assert.containsOnce(target, dsHelpers.SELECTORS.addNewRule);
// Clicking on the button should add the [["id", "=", "1"]] domain, so
// there should be a field selector in the DOM
await dsHelpers.addNewRule(target);
assert.containsOnce(target, ".o_model_field_selector");
// Focusing the field selector input should open the field selector
// popover
await click(target, ".o_model_field_selector");
assert.containsOnce(document.body, ".o_model_field_selector_popover");
assert.containsOnce(document.body, ".o_model_field_selector_popover_search input");
// The popover should contain the list of partner_type fields and so
// there should be the "Color index" field
assert.strictEqual(
document.body.querySelector(".o_model_field_selector_popover_item_name").textContent,
"Color index"
);
// Clicking on this field should close the popover, then changing the
// associated value should reveal one matched record
await click(document.body.querySelector(".o_model_field_selector_popover_item_name"));
await dsHelpers.editValue(target, 2);
assert.strictEqual(
target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2),
"1 ",
"changing color value to 2 should reveal only one record"
);
// Saving the form view should show a readonly domain containing the
// "color" field
await clickSave(target);
assert.ok(target.querySelector(".o_field_domain").textContent.includes("Color index"));
// Fold domain selector
await click(target, ".o_field_domain a i");
assert.containsOnce(target, ".o_field_domain .o_facet_values:contains('Color index = 2')");
});
QUnit.test("add condition in empty foldable domain", async function (assert) {
patchWithCleanup(odoo, { debug: true });
serverData.models.partner.records[0].foo = '[("id", "=", 1)]';
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'foldable': true}" />
</group>
</sheet>
</form>`,
});
// As the domain is not empty, the "Add condition" button should not be available
assert.containsNone(target, ".o_domain_add_first_node_button");
// Unfold the domain and delete the condition
await click(target, ".o_field_domain > div > div");
await dsHelpers.clickOnButtonDeleteNode(target);
// Fold domain selector
await click(target, ".o_field_domain a i");
// As the domain is empty, the "Add condition" button should now be available
assert.containsOnce(target, ".o_domain_add_first_node_button");
// Click on "Add condition"
await click(target, ".o_domain_add_first_node_button");
// Domain is now unfolded with the default condition
assert.containsOnce(target, ".o_model_field_selector");
assert.strictEqual(
target.querySelector(dsHelpers.SELECTORS.debugArea).value,
'[("id", "=", 1)]'
);
});
QUnit.test(
"foldable domain field unfolds and hides caret when domain is invalid",
async function (assert) {
serverData.models.partner.records[0].foo = "[";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'foldable': true}" />
</group>
</sheet>
</form>`,
});
assert.strictEqual(
target.querySelector(".o_field_domain span").textContent,
" Invalid domain "
);
assert.containsNone(target, ".fa-caret-down");
assert.strictEqual(
target.querySelector(".o_domain_selector_row").textContent,
" This domain is not supported. Reset domain"
);
await click(target, ".o_domain_selector_row button");
assert.strictEqual(
target.querySelector(".o_field_domain span").textContent,
"Match all records"
);
}
);
QUnit.test("allow_expressions = true", async function (assert) {
serverData.models.partner.records[0].foo = "[]";
patchWithCleanup(odoo, { debug: true });
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type', 'allow_expressions':True}" />
</group>
</sheet>
</form>`,
mockRPC(route) {
if (route === "/web/domain/validate") {
return true;
}
},
context: { path: "name", name: "name" },
});
await editInput(target, dsHelpers.SELECTORS.debugArea, `[("name", "=", [name])]`);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain involves non-literals. Their evaluation might fail."]);
await editInput(
target,
dsHelpers.SELECTORS.debugArea,
`["&", ("name", "=", "name"), (path, "=", "other name")]`
);
assert.doesNotHaveClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain involves non-literals. Their evaluation might fail."]);
});
QUnit.test("allow_expressions = false (default)", async function (assert) {
serverData.models.partner.records[0].foo = "[]";
patchWithCleanup(odoo, { debug: true });
replaceNotificationService(assert);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" widget="domain" options="{'model': 'partner_type' }" />
</group>
</sheet>
</form>`,
context: { path: "name", name: "name" },
});
await editInput(target, dsHelpers.SELECTORS.debugArea, `[("name", "=", name)]`);
assert.hasClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain should not involve non-literals"]);
await editInput(
target,
dsHelpers.SELECTORS.debugArea,
`["&", ("name", "=", "name"), (path, "=", 1)]`
);
assert.hasClass(target.querySelector(".o_field_domain"), "o_field_invalid");
assert.verifySteps(["The domain should not involve non-literals"]);
});
});

View file

@ -0,0 +1,164 @@
/** @odoo-module **/
import { click, getFixture, triggerEvent, triggerHotkey } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module("Dynamic placeholder", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
char: {
string: "Char",
type: "char",
},
placeholder: {
string: "Placeholder",
type: "char",
default: "partner",
},
product_id: {
string: "Product",
type: "many2one",
relation: "product",
searchable: true,
},
},
records: [
{
id: 1,
char: "yop",
product_id: 37,
},
{
id: 2,
char: "blip",
product_id: 41,
},
],
},
product: {
fields: {
name: { string: "Product Name", type: "char", searchable: true },
},
records: [
{
id: 37,
name: "xphone",
},
{
id: 41,
name: "xpad",
},
],
},
},
};
setupViewRegistries();
});
QUnit.test("dynamic placeholder close on click out", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<field name="placeholder" invisible="1"/>
<sheet>
<group>
<field
name="char"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'placeholder'
}"
/>
</group>
</sheet>
</form>
`,
});
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
assert.containsOnce(target, ".o_model_field_selector_popover");
await click(target, ".o_content");
assert.containsNone(target, ".o_model_field_selector_popover");
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
await click(target, ".o_model_field_selector_popover_item_relation");
await click(target, ".o_content");
assert.containsNone(target, ".o_model_field_selector_popover");
});
QUnit.test("dynamic placeholder close with escape", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<field name="placeholder" invisible="1"/>
<sheet>
<group>
<field
name="char"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'placeholder'
}"
/>
</group>
</sheet>
</form>
`,
});
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
assert.containsOnce(target, ".o_model_field_selector_popover");
await triggerHotkey("Escape");
assert.containsNone(target, ".o_model_field_selector_popover");
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
await click(target, ".o_model_field_selector_popover_item_relation");
await triggerHotkey("Escape");
assert.containsNone(target, ".o_model_field_selector_popover");
});
QUnit.test("dynamic placeholder close when clicking on the cross", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<field name="placeholder" invisible="1"/>
<sheet>
<group>
<field
name="char"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'placeholder'
}"
/>
</group>
</sheet>
</form>
`,
});
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
assert.containsOnce(target, ".o_model_field_selector_popover");
await click(target, ".o_model_field_selector_popover_close");
assert.containsNone(target, ".o_model_field_selector_popover");
await triggerEvent(target, ".o_field_char input", "keydown", { key: "#" });
await click(target, ".o_model_field_selector_popover_item_relation");
await click(target, ".o_model_field_selector_popover_close");
assert.containsNone(target, ".o_model_field_selector_popover");
});
});

View file

@ -75,6 +75,12 @@ QUnit.module("Fields", (hooks) => {
"should have rendered the email button as a link with correct classes"
);
assert.hasAttrValue(emailBtn, "href", "mailto:yop", "should have proper mailto prefix");
assert.hasAttrValue(
emailBtn,
"target",
"_blank",
"should have target attribute set to _blank"
);
// change value in edit mode
await editInput(target, ".o_field_email input[type='email']", "new");

View file

@ -0,0 +1,123 @@
/** @odoo-module **/
import { editSelect, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
// Note: the containsN always check for one more as there will be an invisible empty option every time.
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
program: {
fields: {
program_type: {
type: "selection",
selection: [
["coupon", "Coupons"],
["promotion", "Promotion"],
["gift_card", "gift_card"],
],
required: true,
}
},
records: [
{ id: 1, program_type: "coupon" },
{ id: 2, program_type: "gift_card" },
],
},
}
}
setupViewRegistries();
});
QUnit.module("utils");
QUnit.test("FilterableSelectionField test whitelist", async (assert) => {
await makeView({
type: "form",
resModel: "program",
resId: 1,
serverData,
arch: `
<form>
<field name="program_type" widget="filterable_selection" options="{'whitelisted_values': ['coupons', 'promotion']}"/>
</form>`,
});
assert.containsN(target, "select option", 3);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
});
QUnit.test("FilterableSelectionField test blacklist", async (assert) => {
await makeView({
type: "form",
resModel: "program",
resId: 1,
serverData,
arch: `
<form>
<field name="program_type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
</form>`,
});
assert.containsN(target, "select option", 3);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
});
QUnit.test("FilterableSelectionField test with invalid value", async (assert) => {
// The field should still display the current value in the list
await makeView({
type: "form",
resModel: "program",
resId: 2,
serverData,
arch: `
<form>
<field name="program_type" widget="filterable_selection" options="{'blacklisted_values': ['gift_card']}"/>
</form>`,
});
assert.containsN(target, "select option", 4);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"gift_card\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
await editSelect(target, ".o_field_widget[name='program_type'] select", '"coupon"');
assert.containsN(target, "select option", 3);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"coupon\"']",
);
assert.containsOnce(
target,
".o_field_widget[name='program_type'] select option[value='\"promotion\"']",
);
});
});

View file

@ -1,7 +1,9 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
let serverData;
let target;
@ -39,7 +41,7 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
// 2.3 / 0.5 = 4.6
assert.strictEqual(args[1].qux, 4.6, "the correct float value should be saved");
}
@ -60,4 +62,41 @@ QUnit.module("Fields", (hooks) => {
"The new value should be saved and displayed properly."
);
});
QUnit.test("FloatFactorField comma as decimal point", async function (assert) {
assert.expect(3);
registry.category("services").remove("localization");
registry.category("services").add(
"localization",
makeFakeLocalizationService({
decimalPoint: ",",
thousandsSep: "",
})
);
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/web_save") {
// 2.3 / 0.5 = 4.6
assert.strictEqual(args[1].qux, 4.6, "the correct float value should be saved");
assert.step("save");
}
},
});
await editInput(target, ".o_field_widget[name='qux'] input", "2,3");
await clickSave(target);
assert.verifySteps(["save"]);
});
});

View file

@ -1,5 +1,6 @@
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
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";
@ -24,6 +25,9 @@ QUnit.module("Fields", (hooks) => {
{ id: 3, float_field: -3.89859 },
{ id: 4, float_field: false },
{ id: 5, float_field: 9.1 },
{ id: 100, float_field: 2.034567e3 },
{ id: 101, float_field: 3.75675456e6 },
{ id: 102, float_field: 6.67543577586e12 },
],
},
},
@ -34,6 +38,66 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("FloatField");
QUnit.test("human readable format 1", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 101,
arch: `<form><field name="float_field" options="{'human_readable': 'true'}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"4M",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 2", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 100,
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"2.0k",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 3", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"6.6754T",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("still human readable when readonly", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field readonly="true" name="float_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget span").textContent,
"6.6754T",
"The value should be rendered in human readable format when input is readonly."
);
});
QUnit.test("unset field should be set to 0", async function (assert) {
await makeView({
type: "form",
@ -251,8 +315,7 @@ QUnit.module("Fields", (hooks) => {
});
// switch to edit mode
var cell = target.querySelector("tr.o_data_row td:not(.o_list_record_selector)");
await click(cell);
await click(target.querySelector("tr.o_data_row td:not(.o_list_record_selector)"));
assert.containsOnce(
target,
@ -268,7 +331,11 @@ QUnit.module("Fields", (hooks) => {
);
await editInput(target, 'div[name="float_field"] input', "18.8958938598598");
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelector(".o_field_widget").textContent,
"18.896",
@ -389,6 +456,81 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("field with enable_formatting option as false", async function (assert) {
registry.category("services").remove("localization");
registry
.category("services")
.add(
"localization",
makeFakeLocalizationService({ thousandsSep: ",", grouping: [3, 0] })
);
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `<form><field name="float_field" options="{'enable_formatting': false}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"0.36",
"Integer value must not be formatted"
);
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",
"Integer value must be not formatted if input type is number."
);
});
QUnit.test(
"field with enable_formatting option as false 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]" options="{'enable_formatting': false}" />
</tree>`,
});
// switch to edit mode
await click(target.querySelector("tr.o_data_row td:not(.o_list_record_selector)"));
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.2458938598598",
"The value should not be formatted on blur."
);
await editInput(target, 'div[name="float_field"] input', "18.8958938598598");
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelector(".o_field_widget").textContent,
"18.8958938598598",
"The new value should not be rounded as well."
);
}
);
QUnit.test("float_field field with placeholder", async function (assert) {
await makeView({
type: "form",
@ -407,14 +549,17 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("float field can be updated by another field/widget", async function (assert) {
class MyWidget extends owl.Component {
class MyWidget extends Component {
static template = xml`<button t-on-click="onClick">do it</button>`;
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);
const myWidget = {
component: MyWidget,
};
registry.category("view_widgets").add("wi", myWidget);
await makeView({
type: "form",
resModel: "partner",

View file

@ -39,7 +39,7 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
// 48 / 60 = 0.8
assert.strictEqual(
args.args[1].qux,
@ -89,7 +89,7 @@ QUnit.module("Fields", (hooks) => {
<field name="qux" widget="float_time"/>
</form>`,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner/write") {
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.strictEqual(
args.args[1].qux,
9.5,

View file

@ -35,7 +35,7 @@ QUnit.module("Fields", (hooks) => {
<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") {
if (route === "/web/dataset/call_kw/partner/web_save") {
// 1.000 / 0.125 = 8
assert.step(args[1].float_field.toString());
}

View file

@ -1,9 +1,10 @@
/** @odoo-module **/
import { markup } from "@odoo/owl";
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { currencies } from "@web/core/currency";
import { localization } from "@web/core/l10n/localization";
import { session } from "@web/session";
import {
formatFloat,
formatFloatFactor,
@ -14,6 +15,7 @@ import {
formatMonetary,
formatPercentage,
formatReference,
formatText,
formatX2many,
} from "@web/views/fields/formatters";
@ -26,135 +28,6 @@ QUnit.module("Fields", (hooks) => {
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) {
@ -234,10 +107,21 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("formatMany2one", function (assert) {
assert.strictEqual(formatMany2one(false), "");
assert.strictEqual(formatMany2one([false, "M2O value"]), "M2O value");
assert.strictEqual(formatMany2one([1, false]), "Unnamed");
assert.strictEqual(formatMany2one([1, "M2O value"]), "M2O value");
assert.strictEqual(formatMany2one([1, "M2O value"], { escape: true }), "M2O%20value");
});
QUnit.test("formatText", function (assert) {
assert.strictEqual(formatText(false), "");
assert.strictEqual(formatText("value"), "value");
assert.strictEqual(formatText(1), "1");
assert.strictEqual(formatText(1.5), "1.5");
assert.strictEqual(formatText(markup("<p>This is a Test</p>")), "<p>This is a Test</p>");
assert.strictEqual(formatText([1, 2, 3, 4, 5]), "1,2,3,4,5");
assert.strictEqual(formatText({ a: 1, b: 2 }), "[object Object]");
});
QUnit.test("formatX2many", function (assert) {
// Results are cast as strings since they're lazy translated.
assert.strictEqual(String(formatX2many({ currentIds: [] })), "No records");
@ -246,7 +130,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("formatMonetary", function (assert) {
patchWithCleanup(session.currencies, {
patchWithCleanup(currencies, {
10: {
digits: [69, 2],
position: "after",
@ -265,67 +149,24 @@ QUnit.module("Fields", (hooks) => {
});
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"
);
const field = {
type: "monetary",
currency_field: "c_x",
};
let data = {
c_x: [11],
c_y: 12,
};
assert.deepEqual(formatMonetary(200, { field, currencyId: 10, data }), "200.00\u00a0€");
assert.deepEqual(formatMonetary(200, { field, data }), "$\u00a0200.00");
assert.deepEqual(formatMonetary(200, { field, currencyField: "c_y", data }), "200.00\u00a0&");
// 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");
const floatField = { type: "float" };
data = {
currency_id: [11],
};
assert.deepEqual(formatMonetary(200, { field: floatField, data }), "$\u00a0200.00");
});
QUnit.test("formatPercentage", function (assert) {

View file

@ -0,0 +1,101 @@
/** @odoo-module **/
import { onMounted } from "@odoo/owl";
import { getFixture, getNodesTextContent, patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { GaugeField } from "@web/views/fields/gauge/gauge_field";
let serverData;
let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
int_field: {
string: "int_field",
type: "integer",
},
another_int_field: {
string: "another_int_field",
type: "integer",
},
},
records: [
{ id: 1, int_field: 10, another_int_field: 45 },
{ id: 2, int_field: 4, another_int_field: 10 },
],
},
},
};
setupViewRegistries();
});
QUnit.module("GaugeField");
QUnit.test("GaugeField in kanban view", async function (assert) {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<field name="another_int_field"/>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" widget="gauge" options="{'max_field': 'another_int_field'}"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2);
assert.containsN(target, ".o_field_widget[name=int_field] .oe_gauge canvas", 2);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_gauge_value")), [
"10",
"4",
]);
});
QUnit.test("GaugeValue supports max_value option", async function (assert) {
patchWithCleanup(GaugeField.prototype, {
setup() {
super.setup();
onMounted(() => {
assert.step("gauge mounted");
assert.strictEqual(this.chart.config.options.plugins.tooltip.callbacks.label({}), "Max: 120");
});
}
});
serverData.models.partner.records = serverData.models.partner.records.slice(0,1);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" widget="gauge" options="{'max_value': 120}"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.verifySteps(["gauge mounted"]);
assert.containsN(target, ".o_field_widget[name=int_field] .oe_gauge canvas", 1);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_gauge_value")), [
"10",
]);
});
});

View file

@ -9,7 +9,7 @@ import {
} 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 { htmlField } from "@web/views/fields/html/html_field";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { session } from "@web/session";
@ -38,7 +38,7 @@ QUnit.module("Fields", ({ beforeEach }) => {
setupViewRegistries();
// Explicitly removed by web_editor, we need to add it back
registry.category("fields").add("html", HtmlField, { force: true });
registry.category("fields").add("html", htmlField, { force: true });
});
QUnit.module("HtmlField");
@ -295,4 +295,99 @@ QUnit.module("Fields", ({ beforeEach }) => {
await click(target, ".modal button.btn-primary"); // save
});
QUnit.test("html fields: spellcheck is disabled on blur", 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.strictEqual(textarea.spellcheck, true, "by default, spellcheck is enabled");
textarea.focus();
await editInput(textarea, null, "nev walue");
textarea.blur();
assert.strictEqual(
textarea.spellcheck,
false,
"spellcheck is disabled once the field has lost its focus"
);
textarea.focus();
assert.strictEqual(
textarea.spellcheck,
true,
"spellcheck is re-enabled once the field is focused"
);
});
QUnit.test(
"Setting an html field to empty string is saved as a false value",
async function (assert) {
assert.expect(1);
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<group>
<field name="txt" />
</group>
</sheet>
</form>`,
resId: 1,
mockRPC(route, { args, method }) {
if (method === "web_save") {
assert.strictEqual(args[1].txt, false, "the txt value should be false");
}
},
});
await editInput(target, ".o_field_widget[name=txt] textarea", "");
await clickSave(target);
}
);
QUnit.test(
"html field: correct value is used to evaluate the modifiers",
async function (assert) {
serverData.models.partner.fields.foo = { string: "foo", type: "char" };
serverData.models.partner.onchanges = {
foo: (obj) => {
if (obj.foo === "a") {
obj.txt = false;
} else if (obj.foo === "b") {
obj.txt = "";
}
},
};
serverData.models.partner.records[0].foo = false;
serverData.models.partner.records[0].txt = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<field name="foo" />
<field name="txt" invisible="'' == txt"/>
</form>`,
});
assert.containsOnce(target, "[name='txt'] textarea");
await editInput(target, "[name='foo'] input", "a");
assert.containsOnce(target, "[name='txt'] textarea");
await editInput(target, "[name='foo'] input", "b");
assert.containsNone(target, "[name='txt'] textarea");
}
);
});

View file

@ -24,7 +24,7 @@ let target;
function getUnique(target) {
const src = target.dataset.src;
return new URL(src).searchParams.get("unique");
return new URL(src, window.location).searchParams.get("unique");
}
QUnit.module("Fields", (hooks) => {
@ -87,7 +87,7 @@ QUnit.module("Fields", (hooks) => {
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].write_date = "2017-02-08 10:00:00";
serverData.models.partner.records[0].document = MY_IMAGE;
await makeView({
@ -99,12 +99,16 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="document" widget="image" options="{'size': [90, 90]}" />
</form>`,
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/read") {
mockRPC(route, { args, kwargs }) {
if (route === "/web/dataset/call_kw/partner/web_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"
kwargs.specification,
{
display_name: {},
document: {},
write_date: {},
},
"The fields document, display_name and write_date should be present when reading an image"
);
}
},
@ -295,7 +299,7 @@ QUnit.module("Fields", (hooks) => {
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000, 1659695820000
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
@ -312,8 +316,8 @@ QUnit.module("Fields", (hooks) => {
<field name="document" widget="image" />
</form>`,
mockRPC(_route, { method, args }) {
if (method === "write") {
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
args[1].write_date = lastUpdates[index];
args[1].document = "4 kb";
index++;
}
@ -381,7 +385,7 @@ QUnit.module("Fields", (hooks) => {
};
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000
const lastUpdates = ["2022-08-05 09:37:00"];
@ -398,8 +402,8 @@ QUnit.module("Fields", (hooks) => {
<field name="document" widget="image" />
</form>`,
mockRPC(_route, { method, args }) {
if (method === "write") {
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
args[1].write_date = lastUpdates[index];
args[1].document = "3 kb";
index++;
}
@ -517,7 +521,7 @@ QUnit.module("Fields", (hooks) => {
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";
serverData.models.partner.records[0].write_date = "2022-08-05 08:37:00";
await makeView({
type: "form",
@ -547,7 +551,7 @@ QUnit.module("Fields", (hooks) => {
"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";
serverData.models.partner.records[0].write_date = "2022-08-05 08:37:00";
await makeView({
type: "form",
@ -583,7 +587,7 @@ QUnit.module("Fields", (hooks) => {
);
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].write_date = "2017-02-08 10:00:00";
serverData.models.partner.records[0].document = MY_IMAGE;
serverData.models.partner_type.fields.image = {
name: "image",
@ -708,7 +712,7 @@ QUnit.module("Fields", (hooks) => {
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));
await new Promise((resolve) => setTimeout(resolve, 100));
// Wait for a render
await nextTick();
}
@ -728,7 +732,7 @@ QUnit.module("Fields", (hooks) => {
);
await clickSave(target);
await click(target, ".o_form_button_create");
await click(target, ".o_control_panel_main_buttons .d-none .o_form_button_create");
assert.strictEqual(
target.querySelector("img[data-alt='Binary file']").dataset.src,
"/web/static/img/placeholder.png",
@ -751,7 +755,7 @@ QUnit.module("Fields", (hooks) => {
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.document = "3 kb";
rec.__last_update = "2022-08-05 08:37:00";
rec.write_date = "2022-08-05 08:37:00";
await makeView({
resId: 1,
@ -765,14 +769,14 @@ QUnit.module("Fields", (hooks) => {
</form>`,
mockRPC(route, { method, args }) {
assert.step(method);
if (method === "write") {
if (method === "web_save") {
// 1659692220000
args[1].__last_update = "2022-08-05 09:37:00";
args[1].write_date = "2022-08-05 09:37:00";
}
},
});
assert.verifySteps(["get_views", "read"]);
assert.verifySteps(["get_views", "web_read"]);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
assert.verifySteps([]);
@ -785,7 +789,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
await clickSave(target);
assert.verifySteps(["write", "read"]);
assert.verifySteps(["web_save"]);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
});
@ -793,11 +797,11 @@ QUnit.module("Fields", (hooks) => {
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";
rec.write_date = "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";
rec2.write_date = "2022-08-05 09:37:00";
await makeView({
resIds: [1, 2],
@ -822,11 +826,11 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"unique in url does not change on record change if no_reload option is set",
"unique in url does not change on record change if reload option is set to false",
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";
rec.write_date = "2022-08-05 08:37:00";
await makeView({
resIds: [1, 2],
@ -836,8 +840,8 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="document" widget="image" required="1" options="{'no_reload': true}" />
<field name="__last_update" />
<field name="document" widget="image" required="1" options="{'reload': false}" />
<field name="write_date" />
</form>`,
});
@ -850,12 +854,7 @@ QUnit.module("Fields", (hooks) => {
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 editInput(target, "div[name='write_date'] > div > input", "2022-08-05 08:39:00");
await click(target, ".o_form_button_save");
assert.strictEqual(
getUnique(target.querySelector(".o_field_image img")),
@ -895,7 +894,7 @@ QUnit.module("Fields", (hooks) => {
],
};
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
serverData.models.partner.records[0].write_date = "2017-02-08 10:00:00";
patchDate(2017, 1, 6, 11, 0, 0);
@ -906,16 +905,15 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" />
<field name="user"/>
<field name="related" widget="image"/>
</group>
</sheet>
<field name="foo" />
<field name="user"/>
<field name="related" widget="image"/>
</form>`,
async mockRPC(route, { args }, performRpc) {
if (route === "/web/dataset/call_kw/partner/read") {
if (
route === "/web/dataset/call_kw/partner/web_read" ||
route === "/web/dataset/call_kw/partner/web_save"
) {
const res = await performRpc(...arguments);
// The mockRPC doesn't implement related fields
res[0].related = "3 kb";

View file

@ -33,6 +33,9 @@ QUnit.module("Fields", (hooks) => {
{ id: 1, int_field: 10 },
{ id: 2, int_field: false },
{ id: 3, int_field: 8069 },
{ id: 100, int_field: 2.034567e3 },
{ id: 101, int_field: 3.75675456e6 },
{ id: 102, int_field: 6.67543577586e12 },
],
},
},
@ -43,6 +46,66 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("IntegerField");
QUnit.test("human readable format 1", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 101,
arch: `<form><field name="int_field" options="{'human_readable': 'true'}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"4M",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 2", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 100,
arch: `<form><field name="int_field" options="{'human_readable': 'true', 'decimals': 1}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"2.0k",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("human readable format 3", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field name="int_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"6.6754T",
"The value should be rendered in human readable format (k, M, G, T)."
);
});
QUnit.test("still human readable when readonly", async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 102,
arch: `<form><field readonly="true" name="int_field" options="{'human_readable': 'true', 'decimals': 4}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget span").textContent,
"6.6754T",
"The value should be rendered in human readable format when input is readonly."
);
});
QUnit.test("should be 0 when unset", async function (assert) {
await makeView({
type: "form",
@ -264,7 +327,11 @@ QUnit.module("Fields", (hooks) => {
"The value should be displayed properly in the input."
);
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelector("td:not(.o_list_record_selector)").textContent,
"-28",
@ -289,6 +356,32 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("IntegerField with enable_formatting option as false", async function (assert) {
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 3,
arch: `<form><field name="int_field" options="{'enable_formatting': false}"/></form>`,
});
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
"8069",
"Integer value must not be formatted"
);
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(
"no need to focus out of the input to save the record after correcting an invalid input",
async function (assert) {
@ -329,16 +422,12 @@ QUnit.module("Fields", (hooks) => {
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");
@ -383,4 +472,25 @@ QUnit.module("Fields", (hooks) => {
await triggerEvent(target, ".o_field_widget input", "keydown", { key: "Enter" });
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
});
QUnit.test("value is formatted on click out (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", "change"); // triggered when clicking out
assert.strictEqual(target.querySelector(".o_field_widget input").value, "8,069");
});
});

View file

@ -1,7 +1,5 @@
/** @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";
@ -85,7 +83,6 @@ QUnit.module("Fields", (hooks) => {
};
setupViewRegistries();
registry.category("services").add("cookie", fakeCookieService);
});
async function reloadKanbanView(target) {

View file

@ -181,7 +181,11 @@ QUnit.module("Fields", (hooks) => {
);
// save and check the result
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.strictEqual(
target.querySelectorAll(".o_field_widget .badge:not(:empty)").length,
3,

View file

@ -51,7 +51,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Many2ManyBinaryField");
QUnit.test("widget many2many_binary", async function (assert) {
assert.expect(24);
assert.expect(21);
const fakeHTTPService = {
start() {
@ -95,8 +95,33 @@ QUnit.module("Fields", (hooks) => {
if (args.method !== "get_views") {
assert.step(route);
}
if (route === "/web/dataset/call_kw/ir.attachment/read") {
assert.deepEqual(args.args[1], ["name", "mimetype"]);
if (args.method === "web_read" && args.model === "turtle") {
assert.deepEqual(args.kwargs.specification, {
display_name: {},
picture_ids: {
fields: {
mimetype: {},
name: {},
},
},
});
}
if (args.method === "web_save" && args.model === "turtle") {
assert.deepEqual(args.kwargs.specification, {
display_name: {},
picture_ids: {
fields: {
mimetype: {},
name: {},
},
},
});
}
if (args.method === "web_read" && args.model === "ir.attachment") {
assert.deepEqual(args.kwargs.specification, {
mimetype: {},
name: {},
});
}
},
});
@ -138,10 +163,7 @@ QUnit.module("Fields", (hooks) => {
"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",
]);
assert.verifySteps(["/web/dataset/call_kw/turtle/web_read"]);
// Set and trigger the change of a file for the input
const fileInput = target.querySelector('input[type="file"]');
@ -181,10 +203,8 @@ QUnit.module("Fields", (hooks) => {
"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",
"/web/dataset/call_kw/ir.attachment/web_read",
"/web/dataset/call_kw/turtle/web_save",
]);
});

View file

@ -1,6 +1,15 @@
/** @odoo-module **/
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import {
click,
clickSave,
editInput,
getFixture,
getNodesTextContent,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -41,7 +50,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("Many2ManyCheckBoxesField", async function (assert) {
serverData.models.partner.records[0].timmy = [12];
const commands = [[[6, false, [12, 14]]], [[6, false, [14]]]];
const commands = [[[4, 14]], [[3, 12]]];
await makeView({
type: "form",
resModel: "partner",
@ -54,8 +63,8 @@ QUnit.module("Fields", (hooks) => {
</group>
</form>`,
mockRPC(route, args) {
if (args.method === "write") {
assert.step("write");
if (args.method === "web_save") {
assert.step("web_save");
assert.deepEqual(args.args[1].timmy, commands.shift());
}
},
@ -82,7 +91,7 @@ QUnit.module("Fields", (hooks) => {
assert.notOk(checkboxes[0].checked);
assert.ok(checkboxes[1].checked);
assert.verifySteps(["write", "write"]);
assert.verifySteps(["web_save", "web_save"]);
});
QUnit.test("Many2ManyCheckBoxesField (readonly)", async function (assert) {
@ -95,7 +104,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" attrs="{'readonly': true}" />
<field name="timmy" widget="many2many_checkboxes" readonly="True" />
</group>
</form>`,
});
@ -125,6 +134,50 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Many2ManyCheckBoxesField does not read added record", async function (assert) {
serverData.models.partner.records[0].timmy = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" />
</group>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget div.form-check input:checked");
await click(target.querySelector("div.o_field_widget div.form-check input"));
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsOnce(target, "div.o_field_widget div.form-check input:checked");
await clickSave(target);
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsOnce(target, "div.o_field_widget div.form-check input:checked");
assert.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
});
QUnit.test(
"Many2ManyCheckBoxesField: start non empty, then remove twice",
async function (assert) {
@ -190,6 +243,32 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test(
"Many2ManyCheckBoxesField: many2many read, field context is properly sent",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_checkboxes" context="{ 'hello': 'world' }" />
</form>`,
mockRPC(route, args) {
if (args.method === "web_read" && args.model === "partner") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.specification.timmy.context.hello, "world");
} else if (args.method === "name_search" && args.model === "partner_type") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.context.hello, "world");
}
},
});
assert.verifySteps(["web_read partner", "name_search partner_type"]);
}
);
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
@ -219,10 +298,8 @@ QUnit.module("Fields", (hooks) => {
<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]]);
if (method === "web_save") {
assert.deepEqual(args[1].timmy, [[3, records[records.length - 1].id]]);
}
},
});
@ -274,11 +351,9 @@ QUnit.module("Fields", (hooks) => {
<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 === "web_save") {
assert.deepEqual(args[1].timmy, [[3, records[0].id]]);
assert.step("web_save");
}
if (method === "name_search") {
assert.step("name_search");
@ -303,7 +378,7 @@ QUnit.module("Fields", (hooks) => {
assert.notOk(
target.querySelector(".o_field_widget[name='timmy'] input[type='checkbox']").checked
);
assert.verifySteps(["name_search", "write"]);
assert.verifySteps(["name_search", "web_save"]);
});
QUnit.test("Many2ManyCheckBoxesField in a one2many", async function (assert) {
@ -326,9 +401,20 @@ QUnit.module("Fields", (hooks) => {
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "write") {
if (args.method === "web_save") {
assert.deepEqual(args.args[1], {
p: [[1, 1, { timmy: [[6, false, [15, 12]]] }]],
p: [
[
1,
1,
{
timmy: [
[4, 12],
[3, 14],
],
},
],
],
});
}
},
@ -352,7 +438,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("Many2ManyCheckBoxesField with default values", async function (assert) {
assert.expect(7);
serverData.models.partner.fields.timmy.default = [3];
serverData.models.partner.fields.timmy.default = [[4, 3]];
serverData.models.partner.fields.timmy.type = "many2many";
serverData.models.partner_type.records.push({ id: 3, display_name: "bronze" });
@ -365,10 +451,10 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_checkboxes"/>
</form>`,
mockRPC: function (route, args) {
if (args.method === "create") {
if (args.method === "web_save") {
assert.deepEqual(
args.args[0].timmy,
[[6, false, [12]]],
args.args[1].timmy,
[[4, 12]],
"correct values should have been sent to create"
);
}
@ -408,4 +494,131 @@ QUnit.module("Fields", (hooks) => {
await clickSave(target);
});
QUnit.test("Many2ManyCheckBoxesField batches successive changes", async function (assert) {
serverData.models.partner.records[0].timmy = [];
serverData.models.partner.onchanges = {
timmy: () => {},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" />
</group>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget div.form-check input:checked");
let mockSetTimeout;
patchWithCleanup(browser, { setTimeout: (fn) => (mockSetTimeout = fn) });
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]);
// checkboxes are updated directly
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
// but no onchanges has been fired yet
assert.verifySteps(["get_views", "web_read", "name_search"]);
// execute the setTimeout callback
mockSetTimeout();
await nextTick();
assert.verifySteps(["onchange"]);
});
QUnit.test("Many2ManyCheckBoxesField sends batched changes on save", async function (assert) {
serverData.models.partner.records[0].timmy = [];
serverData.models.partner.onchanges = {
timmy: () => {},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" />
</group>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsN(target, "div.o_field_widget div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget div.form-check input:checked");
patchWithCleanup(browser, { setTimeout: () => {} }); // never call it
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]);
// checkboxes are updated directly
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
// but no onchanges has been fired yet
assert.verifySteps(["get_views", "web_read", "name_search"]);
// save
await clickSave(target);
assert.verifySteps(["onchange", "web_save"]);
});
QUnit.test("Many2ManyCheckBoxesField in a notebook tab", async function (assert) {
serverData.models.partner.records[0].timmy = [];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<notebook>
<page string="Page 1">
<field name="timmy" widget="many2many_checkboxes" />
</page>
<page string="Page 2">
<field name="int_field" />
</page>
</notebook>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.containsOnce(target, "div.o_field_widget[name=timmy]");
assert.containsN(target, "div.o_field_widget[name=timmy] div.form-check", 2);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_widget .form-check-label")),
["gold", "silver"]
);
assert.containsNone(target, "div.o_field_widget[name=timmy] div.form-check input:checked");
patchWithCleanup(browser, { setTimeout: () => {} }); // never call it
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]);
// checkboxes are updated directly
assert.containsN(target, "div.o_field_widget div.form-check input:checked", 2);
// go to the other tab
await click(target.querySelectorAll(".o_notebook .nav-link")[1]);
assert.containsNone(target, "div.o_field_widget[name=timmy]");
assert.containsOnce(target, "div.o_field_widget[name=int_field]");
// save
await clickSave(target);
assert.verifySteps(["get_views", "web_read", "name_search", "web_save"]);
});
});

View file

@ -1,6 +1,5 @@
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
import {
addRow,
click,
@ -17,10 +16,11 @@ import { editSearch, validateSearch } from "@web/../tests/search/helpers";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { Deferred } from "@web/core/utils/concurrency";
import { session } from "@web/session";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
import { companyService } from "@web/webclient/company_service";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
let target;
let serverData;
@ -222,7 +222,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Many2ManyField");
QUnit.test("many2many kanban: edition", async function (assert) {
assert.expect(31);
assert.expect(29);
serverData.views = {
"partner_type,false,form": '<form><field name="display_name"/></form>',
@ -260,37 +260,41 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner_type/write") {
if (
route === "/web/dataset/call_kw/partner_type/web_save" &&
args.args[0].length !== 0
) {
assert.strictEqual(
args.args[1].display_name,
"new name",
"should write 'new_name'"
);
}
if (route === "/web/dataset/call_kw/partner_type/create") {
if (
route === "/web/dataset/call_kw/partner_type/web_save" &&
args.args[0].length === 0
) {
assert.strictEqual(
args.args[0].display_name,
args.args[1].display_name,
"A new type",
"should create 'A new type'"
);
}
if (route === "/web/dataset/call_kw/partner/write") {
var commands = args.args[1].timmy;
assert.strictEqual(commands.length, 1, "should have generated one command");
assert.strictEqual(
commands[0][0],
6,
"generated command should be REPLACE WITH"
);
if (
route === "/web/dataset/call_kw/partner/web_save" &&
args.args[0].length !== 0
) {
const commands = args.args[1].timmy;
// get the created type's id
var createdType = _.findWhere(serverData.models.partner_type.records, {
display_name: "A new type",
const createdType = serverData.models.partner_type.records.find((record) => {
return record.display_name === "A new type";
});
var ids = _.sortBy([12, 15, 18].concat(createdType.id), _.identity.bind(_));
assert.ok(
_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids),
"new value should be " + ids
);
assert.deepEqual(commands, [
[4, 15],
[4, 18],
[4, createdType.id],
[3, 14],
]);
}
},
});
@ -632,7 +636,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
"many2many list (non editable): create a new record and click on action button",
"many2many list (non editable): create a new record and click on action button 1",
async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
@ -659,8 +663,8 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC: async (route, args) => {
assert.step(args.method);
if (args.method === "create") {
assert.deepEqual(args.args[0], { display_name: "Hello" });
if (args.method === "web_save") {
assert.deepEqual(args.args[1], { display_name: "Hello" });
}
},
});
@ -673,20 +677,25 @@ QUnit.module("Fields", (hooks) => {
let modal = target.querySelector(".modal");
await click(modal, ".o_create_button");
assert.verifySteps(["get_views", "read", "get_views", "web_search_read", "onchange"]);
assert.verifySteps([
"get_views",
"web_read",
"get_views",
"web_search_read",
"onchange",
]);
modal = target.querySelector(".modal");
await editInput(modal, "[name='display_name'] input", "Hello");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
await click(modal, ".o_statusbar_buttons [name='myaction']");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
assert.verifySteps(["create", "read", "action: myaction"]);
assert.verifySteps(["web_save", "action: myaction"]);
}
);
QUnit.test(
"many2many list (non editable): create a new record and click on action button",
"many2many list (non editable): create a new record and click on action button 2",
async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
@ -713,8 +722,8 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC: async (route, args) => {
assert.step(args.method);
if (args.method === "create") {
assert.deepEqual(args.args[0], { display_name: "Hello" });
if (args.method === "web_save" && args.args[0].length === 0) {
assert.deepEqual(args.args[1], { display_name: "Hello" });
}
},
});
@ -727,7 +736,13 @@ QUnit.module("Fields", (hooks) => {
let modal = target.querySelector(".modal");
await click(modal, ".o_create_button");
assert.verifySteps(["get_views", "read", "get_views", "web_search_read", "onchange"]);
assert.verifySteps([
"get_views",
"web_read",
"get_views",
"web_search_read",
"onchange",
]);
modal = target.querySelector(".modal");
await editInput(modal, "[name='display_name'] input", "Hello");
@ -753,10 +768,57 @@ QUnit.module("Fields", (hooks) => {
["Hello (edited)"]
);
assert.verifySteps(["create", "read", "action: myaction", "write", "read", "read"]);
assert.verifySteps(["web_save", "action: myaction", "web_save", "web_read"]);
}
);
QUnit.test("add a new record in a many2many non editable list", async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,form": '<form><field name="display_name"/></form>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
if (args.method === "web_save") {
// should not read the record as we're closing the dialog
assert.deepEqual(args.kwargs.specification, {});
}
},
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
await click(target.querySelector(".o_dialog .o_create_button"));
await editInput(
target.querySelector(".o_dialog"),
".o_field_widget[name=display_name] input",
"a name"
);
await click(target.querySelector(".o_dialog .o_form_button_save"));
assert.verifySteps([
"get_views",
"onchange",
"get_views",
"web_search_read",
"get_views",
"onchange",
"web_save",
"web_read",
]);
});
QUnit.test("add record in a many2many non editable list with context", async function (assert) {
assert.expect(1);
@ -794,10 +856,8 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name=int_field] input", "2");
await click(target.querySelector(".o_field_x2many_list_row_add a"));
});
QUnit.test("many2many list (editable): edition", async function (assert) {
assert.expect(29);
QUnit.test("many2many list (editable): edition concurrence", async function (assert) {
assert.expect(5);
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
serverData.models.partner_type.fields.float_field = { string: "Float", type: "float" };
@ -820,9 +880,49 @@ QUnit.module("Fields", (hooks) => {
</field>
</form>`,
mockRPC(route, args) {
if (args.method !== "get_views") {
assert.step(_.last(route.split("/")));
assert.step(args.method);
if (args.method === "web_save") {
//check that delete command is not duplicate
assert.deepEqual(args.args, [
[1],
{
timmy: [[3, 12]],
},
]);
}
},
resId: 1,
});
const t = target.querySelector(".o_list_record_remove");
click(t);
click(t);
await clickSave(target);
assert.verifySteps(["get_views", "web_read", "web_save"]);
});
QUnit.test("many2many list (editable): edition", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
serverData.models.partner_type.fields.float_field = { string: "Float", type: "float" };
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree editable="top">
<field name="display_name"/>
<field name="float_field"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
assert.step(args.method);
if (args.method === "write") {
assert.deepEqual(args.args[1].timmy, [
[6, false, [12, 15]],
@ -895,7 +995,7 @@ QUnit.module("Fields", (hooks) => {
"new name",
"value of subrecord should have been updated"
);
assert.verifySteps(["read", "read"]);
assert.verifySteps(["get_views", "web_read"]);
// add new subrecords
await click(target.querySelector(".o_field_x2many_list_row_add a"));
@ -943,11 +1043,10 @@ QUnit.module("Fields", (hooks) => {
);
assert.verifySteps([
"get_views", // list view in dialog
"web_search_read", // list view in dialog
"read", // relational field (updated)
"write", // save main record
"read", // main record
"read", // relational field
"web_read", // relational field (updated)
"web_save", // save main record
]);
});
@ -1050,14 +1149,20 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="timmy" widget="many2many" can_create="false" can_write="false"/>
<field name="timmy" widget="many2many" can_create="False" can_write="False"/>
</form>`,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner/create") {
assert.deepEqual(args.args[0], { timmy: [[6, false, [12]]] });
if (
route === "/web/dataset/call_kw/partner/web_save" &&
args.args[0].length === 0
) {
assert.deepEqual(args.args[1], { timmy: [[4, 12]] });
}
if (route === "/web/dataset/call_kw/partner/write") {
assert.deepEqual(args.args[1], { timmy: [[6, false, []]] });
if (
route === "/web/dataset/call_kw/partner/web_save" &&
args.args[0].length !== 0
) {
assert.deepEqual(args.args[1], { timmy: [[3, 12]] });
}
},
});
@ -1377,7 +1482,10 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("many2many list: list of id as default value", async function (assert) {
serverData.models.partner.fields.turtles.default = [2, 3];
serverData.models.partner.fields.turtles.default = [
[4, 2],
[4, 3],
];
serverData.models.partner.fields.turtles.type = "many2many";
await makeView({
@ -1401,6 +1509,49 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test(
"context and domain dependent on an x2m must contain the list of current ids for the x2m",
async function (assert) {
assert.expect(2);
serverData.models.partner.fields.turtles.default = [
[4, 2],
[4, 3],
];
serverData.models.partner.fields.turtles.type = "many2many";
serverData.views = {
"turtle,false,list": '<tree><field name="display_name"/></tree>',
"turtle,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles" context="{'test': turtles}" domain="[('id', 'in', turtles)]">
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "web_search_read") {
assert.deepEqual(args.kwargs.domain, [
"&",
["id", "in", [2, 3]],
"!",
["id", "in", [2, 3]],
]);
assert.deepEqual(args.kwargs.context.test, [2, 3]);
}
},
});
await addRow(target);
}
);
QUnit.test("many2many list with x2many: add a record", async function (assert) {
serverData.models.partner_type.fields.m2m = {
string: "M2M",
@ -1428,10 +1579,7 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC(route, args) {
if (args.method !== "get_views") {
assert.step(_.last(route.split("/")) + " on " + args.model);
}
if (args.model === "turtle") {
assert.step(JSON.stringify(args.args[0])); // the read ids
assert.step(route.split("/").at(-1) + " on " + args.model);
}
},
});
@ -1466,19 +1614,11 @@ QUnit.module("Fields", (hooks) => {
);
assert.verifySteps([
"read on partner",
"web_read on partner",
"web_search_read on partner_type",
"read on turtle",
"[1,2,3]",
"read on partner_type",
"read on turtle",
"[1,2]",
"web_read on partner_type",
"web_search_read on partner_type",
"read on turtle",
"[2,3]",
"read on partner_type",
"read on turtle",
"[2,3]",
"web_read on partner_type",
]);
});
@ -1537,20 +1677,71 @@ QUnit.module("Fields", (hooks) => {
assert.step(args.method);
},
});
assert.verifySteps(["get_views", "read", "read"]);
assert.verifySteps(["get_views", "web_read"]);
await click($(target).find("td.o_data_cell:first")[0]);
assert.verifySteps(["get_views", "read"]);
await click(target.querySelector("td.o_data_cell"));
assert.verifySteps(["get_views", "web_read"]);
await click($('.modal-body input[type="checkbox"]')[0]);
await click($(".modal .modal-footer .btn-primary").first()[0]);
assert.verifySteps(["write", "onchange", "read"]);
await click(target.querySelector(".modal-body input[type=checkbox]"));
await click(target.querySelector(".modal .modal-footer .btn-primary"));
assert.verifySteps(["web_save"]);
// there is nothing left to save -> should not do a 'write' RPC
await clickSave(target);
assert.verifySteps([]);
});
QUnit.test("many2many concurrency edition", async function (assert) {
serverData.models.partner.fields.turtles.type = "many2many";
serverData.models.partner.onchanges.turtles = function () {};
serverData.models.turtle.records.push({
id: 4,
display_name: "Bloop",
turtle_bar: true,
turtle_foo: "Bloop",
partner_ids: [],
});
serverData.models.partner.records[0].turtles = [1, 2, 3, 4];
serverData.views = {
"turtle,false,list": '<tree><field name="display_name"/></tree>',
"turtle,false,search": '<search><field name="display_name" string="Name"/></search>',
};
const def = new Deferred();
let firstOnChange = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles">
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
resId: 1,
mockRPC: async (route, args) => {
if (args.method === "onchange") {
if (!firstOnChange) {
firstOnChange = true;
await def;
}
}
},
});
assert.containsN(target, ".o_data_row", 4);
await click(target.querySelector(".o_data_row .o_list_record_remove"));
await click(target.querySelector(".o_data_row .o_list_record_remove"));
await click(target, ".o_field_x2many_list_row_add a");
await click(target.querySelectorAll(".modal .o_data_row td.o_data_cell")[0]);
def.resolve();
await nextTick();
assert.containsN(target, ".o_data_row", 3);
});
QUnit.test(
"many2many widget: creates a new record with a context containing the parentID",
async function (assert) {
@ -1584,13 +1775,18 @@ QUnit.module("Fields", (hooks) => {
{},
[],
{
turtle_trululu: "",
turtle_foo: {},
turtle_trululu: {
fields: {
display_name: {},
},
},
},
]);
}
},
});
assert.verifySteps(["get_views", "read", "read"]);
assert.verifySteps(["get_views", "web_read"]);
await addRow(target);
assert.verifySteps(["get_views", "web_search_read"]);
@ -1607,10 +1803,10 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("onchange with 40+ commands for a many2many", async function (assert) {
// this test ensures that the basic_model correctly handles more LINK_TO
// commands than the limit of the dataPoint (40 for x2many kanban)
assert.expect(25);
assert.expect(20);
// create a lot of partner_types that will be linked by the onchange
var commands = [[5]];
const commands = [];
for (var i = 0; i < 45; i++) {
var id = 100 + i;
serverData.models.partner_type.records.push({ id: id, display_name: "type " + id });
@ -1643,22 +1839,20 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
mockRPC(route, args) {
assert.step(args.method);
if (args.method === "write") {
assert.strictEqual(args.args[1].timmy[0][0], 6, "should send a command 6");
assert.strictEqual(
args.args[1].timmy[0][2].length,
45,
"should replace with 45 ids"
if (args.method === "web_save") {
assert.deepEqual(
args.args[1].timmy,
commands.map((c) => [c[0], c[1]]),
"should send all commands"
);
}
},
});
assert.verifySteps(["get_views", "read"]);
assert.verifySteps(["get_views", "web_read"]);
await editInput(target, ".o_field_widget[name=foo] input", "trigger onchange");
assert.verifySteps(["onchange", "read"]);
assert.verifySteps(["onchange"]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"1-40 / 45",
@ -1669,9 +1863,8 @@ QUnit.module("Fields", (hooks) => {
40,
"there should be 40 records displayed on page 1"
);
await click($(target).find(".o_field_widget[name=timmy] .o_pager_next")[0]);
assert.verifySteps(["read"]);
assert.verifySteps([]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"41-45 / 45",
@ -1720,21 +1913,15 @@ QUnit.module("Fields", (hooks) => {
"there should be 40 records displayed on page 1"
);
assert.verifySteps(["write", "read", "read", "read"]);
assert.verifySteps(["web_save", "web_read"]);
});
QUnit.test("default_get, onchange, onchange on m2m", async function (assert) {
assert.expect(1);
serverData.models.partner.onchanges.int_field = function (obj) {
if (obj.int_field === 2) {
assert.deepEqual(obj.timmy, [
[6, false, [12]],
[1, 12, { display_name: "gold" }],
]);
}
obj.timmy = [[5], [1, 12, { display_name: "gold" }]];
};
serverData.models.partner.onchanges.int_field = function () {};
let firstOnChange = true;
await makeView({
type: "form",
@ -1751,14 +1938,30 @@ QUnit.module("Fields", (hooks) => {
<field name="int_field"/>
</sheet>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
if (firstOnChange) {
firstOnChange = false;
return {
value: {
timmy: [[1, 12, { display_name: "gold" }]],
},
};
} else {
assert.deepEqual(args.args[1], {
display_name: false,
int_field: 2,
timmy: [[1, 12, { display_name: "gold" }]],
});
}
}
},
});
await editInput(target, ".o_field_widget[name=int_field] input", 2);
});
QUnit.test("many2many list add *many* records, remove, re-add", async function (assert) {
assert.expect(5);
serverData.models.partner.fields.timmy.domain = [["color", "=", 2]];
serverData.models.partner.fields.timmy.onChange = true;
serverData.models.partner_type.fields.product_ids = {
@ -1767,8 +1970,8 @@ QUnit.module("Fields", (hooks) => {
relation: "product",
};
for (var i = 0; i < 50; i++) {
var new_record_partner_type = { id: 100 + i, display_name: "batch" + i, color: 2 };
for (let i = 0; i < 50; i++) {
const new_record_partner_type = { id: 100 + i, display_name: "batch" + i, color: 2 };
serverData.models.partner_type.records.push(new_record_partner_type);
}
@ -1806,7 +2009,7 @@ QUnit.module("Fields", (hooks) => {
// First round: add 51 records in batch
await click(target.querySelector(".o_field_x2many_list_row_add a"));
var $modal = $(".modal-lg");
let $modal = $(".modal-lg");
assert.equal($modal.length, 1, "There should be one modal");
@ -1821,26 +2024,43 @@ QUnit.module("Fields", (hooks) => {
"We should have added all the records present in the search view to the m2m field"
); // the 50 in batch + 'gold'
assert.containsNone(
target,
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_cp_pager",
"pager should not be displayed"
);
await clickSave(target);
assert.containsOnce(
target,
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_cp_pager",
"pager should not be displayed"
);
const pagerValue = target.querySelector(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_pager_value"
);
assert.strictEqual(pagerValue.textContent, "1-40", "The pager should be updated.");
// Secound round: remove one record
var trash_buttons = $(target).find(
const trash_buttons = $(target).find(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_list_record_remove"
);
await click(trash_buttons.first()[0]);
var pager_limit = $(target).find(
const pager_limit = $(target).find(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_pager_limit"
);
assert.equal(pager_limit.text(), "50", "We should have 50 records in the m2m field");
assert.strictEqual(pager_limit.text(), "50", "We should have 50 records in the m2m field");
// Third round: re-add 1 records
await click($(target).find(".o_field_x2many_list_row_add a")[0]);
$modal = $(".modal-lg");
assert.equal($modal.length, 1, "There should be one modal");
assert.strictEqual($modal.length, 1, "There should be one modal");
await click($modal.find("thead input[type=checkbox]")[0]);
await nextTick();
@ -1849,8 +2069,8 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
$(target).find(".o_data_row").length,
51,
"We should have 51 records in the m2m field"
41,
"We should have 41 records in the m2m field"
);
});
@ -1931,12 +2151,13 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("many2many basic keys in field evalcontext -- in list", async (assert) => {
assert.expect(6);
assert.expect(5);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.models.partner.records.push({ id: 7, display_name: "default partner" });
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
@ -1964,13 +2185,12 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': uid, 'allowed_company_ids': allowed_company_ids, 'company_id': current_company_id}"/>
</tree>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.uid, 7);
assert.deepEqual(args.kwargs.context.allowed_company_ids, [3]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
@ -1978,22 +2198,22 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelector(".o_data_cell"));
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await nextTick();
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
"default partner"
);
});
QUnit.test("many2many basic keys in field evalcontext -- in form", async (assert) => {
assert.expect(6);
assert.expect(5);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.models.partner.records.push({ id: 7, display_name: "default partner" });
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
@ -2022,13 +2242,12 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': uid, 'allowed_company_ids': allowed_company_ids, 'company_id': current_company_id}"/>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.default_partner_id, 7);
assert.deepEqual(args.kwargs.context.allowed_company_ids, [3]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
@ -2040,19 +2259,20 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
"default partner"
);
});
QUnit.test(
"many2many basic keys in field evalcontext -- in a x2many in form",
async (assert) => {
assert.expect(6);
assert.expect(5);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.models.partner.records.push({ id: 7, display_name: "default partner" });
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
@ -2085,15 +2305,14 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="p">
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': uid, 'allowed_company_ids': allowed_company_ids, 'company_id': current_company_id}"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.default_partner_id, 7);
assert.deepEqual(args.kwargs.context.allowed_company_ids, [3]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
@ -2105,73 +2324,44 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
"default partner"
);
}
);
QUnit.test("many2many field calling replaceWith (add + remove)", async function (assert) {
serverData.models.partner.records[0].p = [1];
QUnit.test(
"`this` inside rendererProps should reference the component",
async function (assert) {
class CustomX2manyField extends X2ManyField {
setup() {
super.setup();
this.selectCreate = (params) => {
assert.step("selectCreate");
assert.strictEqual(this.num, 2);
};
this.num = 1;
}
class MyX2Many extends Component {
onClick() {
this.props.value.replaceWith([2, 3]);
async onAdd({ context, editable } = {}) {
this.num = 2;
assert.step("onAdd");
super.onAdd(...arguments);
}
}
}
MyX2Many.template = xml`
<span class="ids" t-esc="this.props.value.resIds"/>
<button class="my_btn" t-on-click="onClick">To id</button>`;
registry.category("fields").add("my_x2many", MyX2Many);
const customX2ManyField = {
...x2ManyField,
component: CustomX2manyField,
};
registry.category("fields").add("custom", customX2ManyField);
await makeView({
type: "form",
resModel: "turtle",
serverData,
arch: `
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="partner_ids" widget="my_x2many"/>
</form>`,
resId: 2,
});
assert.strictEqual(target.querySelector(".ids").innerText, "2,4");
await click(target.querySelector(".my_btn"));
assert.strictEqual(target.querySelector(".ids").innerText, "2,3");
});
QUnit.test("`this` inside rendererProps should reference the component", async function (assert) {
class CustomX2manyField extends X2ManyField {
setup() {
super.setup();
this.selectCreate = (params) => {
assert.step("selectCreate");
assert.strictEqual(this.num, 2);
};
this.num = 1;
}
async onAdd({ context, editable } = {}) {
this.num = 2;
assert.step("onAdd");
super.onAdd(...arguments);
}
}
registry.category("fields").add("custom_x2many", CustomX2manyField);
serverData.views = {
"partner_type,false,list": `<tree><field name="display_name"/></tree>`,
"partner_type,false,search": `<search></search>`,
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="custom_x2many">
<field name="timmy" widget="custom">
<tree editable="top">
<field name="display_name"/>
</tree>
@ -2180,9 +2370,10 @@ QUnit.module("Fields", (hooks) => {
</form>
</field>
</form>`,
resId: 1,
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
assert.verifySteps(["onAdd", "selectCreate"]);
});
resId: 1,
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
assert.verifySteps(["onAdd", "selectCreate"]);
}
);
});

View file

@ -1,6 +1,16 @@
/** @odoo-module **/
import { click, clickSave, getFixture, selectDropdownItem } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import {
click,
clickSave,
getFixture,
patchWithCleanup,
selectDropdownItem,
triggerEvent,
editInput,
clickOpenedDropdownItem,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { triggerHotkey } from "../../helpers/utils";
@ -59,13 +69,13 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_avatar img",
2,
"should have 2 records"
);
assert.strictEqual(
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge img").dataset
.src,
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .o_avatar img")
.dataset.src,
"/web/image/partner/2/avatar_128",
"should have correct avatar image"
);
@ -116,13 +126,13 @@ QUnit.module("Fields", (hooks) => {
);
assert.containsN(
target,
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img",
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)",
".o_data_row:nth-child(3) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img",
5,
"should have 5 records"
);
@ -149,21 +159,21 @@ QUnit.module("Fields", (hooks) => {
);
assert.strictEqual(
target.querySelector(
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_tag:nth-child(2) img.o_m2m_avatar"
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar: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"
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar: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"
".o_data_row:nth-child(2) .o_field_many2many_tags_avatar .o_avatar:nth-child(4) img.o_m2m_avatar"
).dataset.src,
"/web/image/partner/5/avatar_128",
"should have correct avatar image"
@ -175,7 +185,7 @@ QUnit.module("Fields", (hooks) => {
);
assert.containsN(
target,
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_tag:not(.o_m2m_avatar_empty)",
".o_data_row:nth-child(4) .o_field_many2many_tags_avatar .o_avatar:not(.o_m2m_avatar_empty) img",
4,
"should have 4 records"
);
@ -213,16 +223,20 @@ QUnit.module("Fields", (hooks) => {
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",
".o_data_row.o_selected_row .o_many2many_tags_avatar_cell .o_avatar img",
1,
"should have 1 many2many badges in edit mode"
);
await selectDropdownItem(target, "partner_ids", "second record");
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.containsN(
target,
".o_data_row:first-child .o_field_many2many_tags_avatar .o_tag",
".o_data_row:first-child .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
@ -273,16 +287,14 @@ QUnit.module("Fields", (hooks) => {
);
QUnit.test("widget many2many_tags_avatar in kanban view", async function (assert) {
assert.expect(13);
assert.expect(21);
const records = [];
for (let id = 5; id <= 15; id++) {
records.push({
serverData.models.partner.records.push({
id,
display_name: `record ${id}`,
});
}
serverData.models.partner.records = serverData.models.partner.records.concat(records);
serverData.models.turtle.records.push({
id: 4,
@ -294,6 +306,8 @@ QUnit.module("Fields", (hooks) => {
serverData.models.turtle.records[2].partner_ids = [1, 2, 4, 5];
serverData.views = {
"turtle,false,form": '<form><field name="display_name"/></form>',
"partner,false,list": '<tree><field name="display_name"/></tree>',
"partner,false,search": "<search/>",
};
await makeView({
@ -325,23 +339,21 @@ QUnit.module("Fields", (hooks) => {
);
},
});
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.containsOnce(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_quick_assign",
"should have the assign icon"
);
assert.containsN(
target,
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_tag",
3,
"should have 3 records"
".o_kanban_record:nth-child(2) .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
assert.containsN(
target,
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_tag",
".o_kanban_record:nth-child(3) .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
@ -349,14 +361,14 @@ QUnit.module("Fields", (hooks) => {
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",
"/web/image/partner/5/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",
"/web/image/partner/4/avatar_128",
"should have correct avatar image"
);
assert.containsOnce(
@ -376,7 +388,7 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_tag",
".o_kanban_record:nth-child(4) .o_field_many2many_tags_avatar .o_avatar img",
2,
"should have 2 records"
);
@ -394,28 +406,94 @@ QUnit.module("Fields", (hooks) => {
"9+",
"should have 9+ in o_m2m_avatar_empty"
);
assert.containsNone(target, ".o_field_many2many_tags_avatar .o_field_many2many_selection");
// 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"
const o_kanban_record = target.querySelector(".o_kanban_record:nth-child(2)");
await click(o_kanban_record, ".o_field_tags > .o_m2m_avatar_empty");
const popover = document.querySelector(".o-overlay-container");
assert.strictEqual(
document.activeElement,
popover.querySelector("input"),
"the input inside the popover should have the focus"
);
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 3, "Should have 3 tags");
// delete inside the popover
await click(popover.querySelector(".o_tag .o_delete"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 2, "Should have 2 tag");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
2,
"Should have 2 tags"
);
// select first input
await click(popover.querySelector(".o-autocomplete--dropdown-item"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 3, "Should have 3 tags");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
2,
"Should have 2 tags"
);
// load more
await click(popover.querySelector(".o_m2o_dropdown_option_search_more"));
// first item
await click(document.querySelector(".o_dialog .o_list_table .o_data_row .o_data_cell"));
assert.strictEqual(popover.querySelectorAll(".o_tag").length, 4, "Should have 4 tags");
assert.strictEqual(
o_kanban_record.querySelectorAll(".o_tag").length,
2,
"Should have 2 tags"
);
assert.strictEqual(
tag.dataset["tooltipTemplate"],
"web.TagsList.Tooltip",
"uses the proper tooltip template"
o_kanban_record.querySelector("img.o_m2m_avatar").dataset.src,
"/web/image/partner/5/avatar_128",
"should have correct avatar image"
);
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 add/remove tags in kanban view",
async function (assert) {
assert.expect(3);
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>`,
async mockRPC(route, { method, args }) {
if (method === "web_save") {
const command = args[1].partner_ids[0];
assert.step(`web_save: ${command[0]}-${command[1]}`);
}
},
});
await click(target, ".o_kanban_record:first-child .o_quick_assign");
// add and directly remove an item
await click(target, ".o_popover .o-autocomplete--dropdown-item:first-child");
await click(target, ".o_popover .o_tag .o_delete");
assert.verifySteps(["web_save: 4-1", "web_save: 3-1"]);
}
);
QUnit.test("widget many2many_tags_avatar delete tag", async function (assert) {
await makeView({
type: "form",
@ -432,25 +510,192 @@ QUnit.module("Fields", (hooks) => {
assert.containsN(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
2,
"should have 2 records"
);
await click(
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .badge .o_delete")
target.querySelector(".o_field_many2many_tags_avatar.o_field_widget .o_tag .o_delete")
);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
"should have 1 record"
);
await clickSave(target);
assert.containsOnce(
target,
".o_field_many2many_tags_avatar.o_field_widget .badge",
".o_field_many2many_tags_avatar.o_field_widget .o_tag",
"should have 1 record"
);
});
QUnit.test(
"widget many2many_tags_avatar quick add tags and close in kanban view with keyboard navigation",
async function (assert) {
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>`,
});
await click(target, ".o_kanban_record:first-child .o_quick_assign");
// add and directly close the dropdown
await triggerEvent(target, null, "keydown", { key: "Tab" });
await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
await triggerEvent(target, null, "keydown", { key: "Escape" });
assert.containsOnce(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_tag",
"should assign the user"
);
assert.containsNone(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_popover",
"should have close the popover"
);
}
);
QUnit.test(
"widget many2many_tags_avatar in kanban view missing access rights",
async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
resModel: "turtle",
serverData,
arch: `
<kanban edit="0" create="0">
<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>`,
});
assert.containsNone(
target,
".o_kanban_record:first-child .o_field_many2many_tags_avatar .o_quick_assign",
"should not have the assign icon"
);
}
);
QUnit.test("widget many2many_tags_avatar", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await makeView({
type: "form",
resModel: "turtle",
serverData,
arch: `
<form>
<sheet>
<field name="partner_ids" widget="many2many_tags_avatar"/>
</sheet>
</form>`,
resId: 1,
});
assert.deepEqual(
[...target.querySelectorAll("[name='partner_ids'] .o_tag")].map((el) => el.textContent),
[]
);
assert.strictEqual(
target.querySelector("[name='partner_ids'] .o_input_dropdown input").value,
""
);
await editInput(target, "[name='partner_ids'] .o_input_dropdown input", "first record");
await triggerEvent(target, "[name='partner_ids'] .o_input_dropdown input", "keydown", {
key: "Enter",
});
assert.deepEqual(
[...target.querySelectorAll("[name='partner_ids'] .o_tag")].map((el) => el.textContent),
["first record"]
);
assert.strictEqual(
target.querySelector("[name='partner_ids'] .o_input_dropdown input").value,
""
);
await editInput(target, "[name='partner_ids'] .o_input_dropdown input", "abc");
await triggerEvent(target, "[name='partner_ids'] .o_input_dropdown input", "keydown", {
key: "Enter",
});
assert.deepEqual(
[...target.querySelectorAll("[name='partner_ids'] .o_tag")].map((el) => el.textContent),
["first record", "abc"]
);
assert.strictEqual(
target.querySelector("[name='partner_ids'] .o_input_dropdown input").value,
""
);
});
QUnit.test(
"Many2ManyTagsAvatarField: make sure that the arch context is passed to the form view call",
async function (assert) {
serverData.views = {
"partner,false,form": `<form><field name="display_name"/></form>`,
};
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await makeView({
type: "list",
resModel: "turtle",
serverData,
arch: `<list editable="top">
<field name="partner_ids" widget="many2many_tags_avatar" context="{ 'append_coucou': 'test_value' }"/>
</list>`,
mockRPC(route, args) {
if (args.method === "onchange" && args.model === "partner") {
if (args.kwargs.context.append_coucou === "test_value") {
assert.step("onchange with context given");
}
}
},
});
await click(target.querySelector("div[name=partner_ids]"));
await editInput(target, `div[name="partner_ids"] input`, "A new partner");
await clickOpenedDropdownItem(target, "partner_ids", "Create and edit...");
assert.containsOnce(target, ".modal .o_form_view", "Here we should have opened the modal form view");
assert.verifySteps(["onchange with context given"]);
}
);
});

View file

@ -1,9 +1,10 @@
/** @odoo-module **/
import { makeServerError } from "@web/../tests/helpers/mock_server";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { browser } from "@web/core/browser/browser";
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
import {
addRow,
click,
clickDiscard,
clickDropdown,
@ -19,7 +20,6 @@ import {
triggerEvent,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { RPCError } from "@web/core/network/rpc_service";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -38,7 +38,6 @@ QUnit.module("Fields", (hooks) => {
string: "one2many turtle field",
type: "one2many",
relation: "turtle",
relation_field: "turtle_trululu",
},
timmy: { string: "pokemon", type: "many2many", relation: "partner_type" },
},
@ -113,13 +112,14 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Many2ManyTagsField");
QUnit.test("Many2ManyTagsField with and without color", async function (assert) {
assert.expect(12);
assert.expect(14);
serverData.models.partner.fields.partner_ids = {
string: "Partner",
type: "many2many",
relation: "partner",
};
serverData.models.partner.fields.color = { string: "Color index", type: "integer" };
await makeView({
type: "form",
@ -130,17 +130,19 @@ QUnit.module("Fields", (hooks) => {
<field name="partner_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="timmy" widget="many2many_tags"/>
</form>`,
mockRPC: (route, { args, method, model }) => {
if (method === "read" && model === "partner_type") {
mockRPC: (route, { args, method, model, kwargs }) => {
if (method === "web_read" && model === "partner_type") {
assert.deepEqual(args, [[12]]);
assert.deepEqual(
args,
[[12], ["display_name"]],
kwargs.specification,
{ display_name: {} },
"should not read any color field"
);
} else if (method === "read" && model === "partner") {
} else if (method === "web_read" && model === "partner") {
assert.deepEqual(args, [[1]]);
assert.deepEqual(
args,
[[1], ["display_name", "color"]],
kwargs.specification,
{ display_name: {}, color: {} },
"should read color field"
);
}
@ -163,8 +165,8 @@ QUnit.module("Fields", (hooks) => {
const autocomplete = target.querySelector("[name='timmy'] .o-autocomplete.dropdown");
assert.strictEqual(
autocomplete.querySelectorAll("li").length,
3,
"autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')"
4,
"autocomplete dropdown should have 4 entries (2 values + 'Search More...' + 'Search and Edit...')"
);
await clickOpenedDropdownItem(target, "timmy", "gold");
assert.containsOnce(target, "[name=timmy] .o_tag");
@ -182,7 +184,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("Many2ManyTagsField with color: rendering and edition", async function (assert) {
assert.expect(26);
assert.expect(24);
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 13, display_name: "red", color: 8 });
@ -195,22 +197,22 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True }"/>
</form>`,
resId: 1,
mockRPC: (route, { args, method, model }) => {
if (route === "/web/dataset/call_kw/partner/write") {
mockRPC: (route, { args, method, model, kwargs }) => {
if (route === "/web/dataset/call_kw/partner/web_save") {
var commands = args[1].timmy;
assert.strictEqual(commands.length, 1, "should have generated one command");
assert.strictEqual(
commands[0][0],
6,
"generated command should be REPLACE WITH"
);
assert.deepEqual(commands[0][2], [12, 13], "new value should be [12, 13]");
}
if (method === "read" && model === "partner_type") {
assert.strictEqual(commands.length, 2, "should have generated two commands");
assert.strictEqual(commands.map((cmd) => cmd[0]).join("-"), "4-3");
assert.deepEqual(
args[1],
["display_name", "color"],
"should read the color field"
commands.map((cmd) => cmd[1]),
[13, 14],
"Should add 13, remove 14"
);
}
if ((method === "web_read" || method === "web_save") && model === "partner_type") {
assert.deepEqual(
kwargs.specification,
{ display_name: {}, color: {} },
"should read color field"
);
}
},
@ -245,8 +247,8 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
autocompleteDropdown.querySelectorAll("li").length,
2,
"autocomplete dropdown should have 2 entry"
3,
"autocomplete dropdown should have 3 entry"
);
assert.strictEqual(
@ -349,14 +351,14 @@ QUnit.module("Fields", (hooks) => {
assert.containsNone(target, ".badge.dropdown-toggle", "the tags should not be dropdowns");
// click on the tag: should do nothing and open the form view
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps(["selectRecord"]);
await nextTick();
assert.containsNone(target, ".o_colorlist");
await click(target.querySelectorAll(".o_list_record_selector")[1]);
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps(["selectRecord"]);
await nextTick();
@ -384,14 +386,14 @@ QUnit.module("Fields", (hooks) => {
assert.containsNone(target, ".badge.dropdown-toggle", "the tags should not be dropdowns");
// click on the tag: should do nothing and open the form view
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps(["selectRecord"]);
await nextTick();
assert.containsNone(target, ".o_colorlist");
await click(target.querySelectorAll(".o_list_record_selector")[1]);
click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
await click(target.querySelector(".o_field_many2many_tags .badge :nth-child(1)"));
assert.verifySteps([]);
await nextTick();
@ -439,8 +441,8 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
autocompleteDropdown.querySelectorAll("li").length,
2,
"autocomplete dropdown should have 2 entry"
3,
"autocomplete dropdown should have 3 entries"
);
assert.strictEqual(
@ -464,6 +466,116 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("use binary field as the domain", async (assert) => {
serverData.models.partner.fields.domain = { string: "Domain", type: "binary" };
serverData.models.partner.records[0].domain = [["id", "<", 50]];
serverData.models.partner.records[0].timmy = [12];
serverData.models.partner_type.records.push({ id: 99, display_name: "red", color: 8 });
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags" domain="domain"/>
<field name="domain" invisible="1"/>
</form>`,
resId: 1,
});
assert.containsOnce(target, ".o_field_many2many_tags .badge", "should contain 1 tag");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".badge")),
["gold"],
"should have fetched and rendered gold partner tag"
);
await clickDropdown(target, "timmy");
const autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
assert.strictEqual(
autocompleteDropdown.querySelectorAll("li").length,
3,
"autocomplete dropdown should have 3 entries"
);
assert.deepEqual(
getNodesTextContent(autocompleteDropdown.querySelectorAll("li")),
["silver", "Search More...", "Start typing..."],
"should contain newly added tag 'silver'"
);
assert.strictEqual(
autocompleteDropdown.querySelector("li a").textContent,
"silver",
"autocomplete dropdown should contain 'silver'"
);
await clickOpenedDropdownItem(target, "timmy", "silver");
assert.strictEqual(
target.querySelectorAll(".o_field_many2many_tags .badge").length,
2,
"should contain 2 tags"
);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".badge")),
["gold", "silver"],
"should contain newly added tag 'silver'"
);
});
QUnit.test("Domain: allow python code domain in fieldInfo", async function (assert) {
assert.expect(4);
serverData.models.partner.fields.timmy.domain =
"foo and [('color', '>', 3)] or [('color', '<', 3)]";
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="foo"/>
<field name="timmy" widget="many2many_tags"></field>
</form>`,
resId: 1,
});
// foo set => only silver (id=5) selectable
await clickDropdown(target, "timmy");
let autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
assert.containsN(
autocompleteDropdown,
"li",
3,
"autocomplete should contain 'silver'm 'Search More...' and 'Start typing...' options"
);
assert.strictEqual(
autocompleteDropdown.querySelector("li a").textContent,
"silver",
"autocomplete dropdown should contain 'silver'"
);
await clickOpenedDropdownItem(target, "timmy", "Start typing...");
// set foo = "" => only gold (id=2) selectable
const textInput = target.querySelector("[name=foo] input");
textInput.focus();
await editInput(textInput, null, "");
await clickDropdown(target, "timmy");
autocompleteDropdown = target.querySelector(".o-autocomplete--dropdown-menu");
assert.containsN(
autocompleteDropdown,
"li",
3,
"autocomplete should contain 'gold'm 'Search More...' and 'Start typing...' options"
);
assert.strictEqual(
autocompleteDropdown.querySelector("li a").textContent,
"gold",
"autocomplete dropdown should contain 'gold'"
);
});
QUnit.test("Many2ManyTagsField in a new record", async function (assert) {
assert.expect(7);
@ -473,15 +585,11 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
mockRPC: (route, { args }) => {
if (route === "/web/dataset/call_kw/partner/create") {
var commands = args[0].timmy;
if (route === "/web/dataset/call_kw/partner/web_save") {
const commands = args[1].timmy;
assert.strictEqual(commands.length, 1, "should have generated one command");
assert.strictEqual(
commands[0][0],
6,
"generated command should be REPLACE WITH"
);
assert.ok(_.isEqual(commands[0][2], [12]), "new value should be [12]");
assert.strictEqual(commands[0][0], 4, "generated command should be LINK TO");
assert.strictEqual(commands[0][1], 12, "new value should be 12");
}
},
});
@ -495,8 +603,8 @@ QUnit.module("Fields", (hooks) => {
const autocomplete = target.querySelector("[name='timmy'] .o-autocomplete.dropdown");
assert.strictEqual(
autocomplete.querySelectorAll("li").length,
3,
"autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')"
4,
"autocomplete dropdown should have 4 entries (2 values + 'Search More...' + 'Search and Edit...')"
);
await clickOpenedDropdownItem(target, "timmy", "gold");
@ -524,7 +632,7 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags" options="{'color_field': 'color'}"/>
</form>`,
mockRPC: (route, { args, method }) => {
if (method === "write") {
if (method === "web_save") {
assert.step(JSON.stringify(args[1]));
}
},
@ -610,7 +718,7 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test("Many2ManyTagsField in editable list", async function (assert) {
assert.expect(7);
assert.expect(5);
serverData.models.partner.records[0].timmy = [12];
@ -624,7 +732,7 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags"/>
</tree>`,
mockRPC: (route, { kwargs, method, model }) => {
if (method === "read" && model === "partner_type") {
if (method === "web_read" && model === "partner_type") {
assert.strictEqual(
kwargs.context.take,
"five",
@ -680,43 +788,10 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test(
"Many2ManyTagsField loads records according to limit defined on widget prototype",
async function (assert) {
patchWithCleanup(Many2ManyTagsField, {
limit: 30,
});
serverData.models.partner.fields.partner_ids = {
string: "Partner",
type: "many2many",
relation: "partner",
};
serverData.models.partner.records[0].partner_ids = [];
for (var i = 15; i < 50; i++) {
serverData.models.partner.records.push({ id: i, display_name: "walter" + i });
serverData.models.partner.records[0].partner_ids.push(i);
}
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="partner_ids" widget="many2many_tags"/></form>',
resId: 1,
});
assert.strictEqual(
target.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length,
30,
"should have rendered 30 tags even though 35 records linked"
);
}
);
QUnit.test("Many2ManyTagsField keeps focus when being edited", async function (assert) {
serverData.models.partner.records[0].timmy = [12];
serverData.models.partner.onchanges.foo = function (obj) {
obj.timmy = [[5]]; // DELETE command
obj.timmy = [[3, 12]];
};
await makeView({
@ -1069,15 +1144,21 @@ QUnit.module("Fields", (hooks) => {
arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
resId: 1,
mockRPC(route, args) {
if (args.method === "read" && args.model === "partner_type") {
assert.step(args.kwargs.context.hello);
if (args.method === "web_read" && args.model === "partner") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.specification.timmy.context.hello, "world");
}
if (args.method === "web_read" && args.model === "partner_type") {
assert.step(`${args.method} ${args.model}`);
assert.strictEqual(args.kwargs.context.hello, "world");
}
},
});
assert.verifySteps(["world"]);
assert.verifySteps(["web_read partner"]);
await selectDropdownItem(target, "timmy", "silver");
assert.verifySteps(["world"]);
assert.verifySteps(["web_read partner_type"]);
});
QUnit.test("Many2ManyTagsField: select multiple records", async function (assert) {
@ -1482,12 +1563,10 @@ QUnit.module("Fields", (hooks) => {
arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
mockRPC(route, args) {
if (args.method === "name_create") {
const error = new RPCError("Something went wrong");
error.exceptionName = "odoo.exceptions.ValidationError";
throw error;
throw makeServerError({ type: "ValidationError" });
}
if (args.method === "create") {
assert.deepEqual(args.args[0], {
if (args.method === "web_save") {
assert.deepEqual(args.args[1], {
color: 8,
name: "new partner",
});
@ -1553,6 +1632,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="timmy" widget="many2many_tags"/>
<field name="name"/>
</tree>`,
});
@ -1620,8 +1700,7 @@ QUnit.module("Fields", (hooks) => {
type: "form",
resModel: "partner",
serverData,
arch:
'<form><field name="timmy" widget="many2many_tags" placeholder="Placeholder"/></form>',
arch: '<form><field name="timmy" widget="many2many_tags" placeholder="Placeholder"/></form>',
});
assert.strictEqual(
@ -1649,7 +1728,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_field_many2many_tags .o-autocomplete--dropdown-menu")
.textContent,
"goldsilver"
"goldsilverSearch More..."
);
});
@ -1674,6 +1753,52 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(target, "[name='timmy'].o_field_invalid");
});
QUnit.test("set a required many2many_tags and save directly", async function (assert) {
let def;
const form = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="timmy" widget="many2many_tags" required="1"/></form>',
async mockRPC(route, args) {
assert.step(args.method);
if (args.method === "web_read") {
await def;
}
},
});
patchWithCleanup(form.env.services.notification, {
add: () => assert.step("notification"),
});
assert.verifySteps(["get_views", "onchange"]);
assert.containsNone(target, ".o_tag");
def = makeDeferred();
await clickDropdown(target, "timmy");
await clickOpenedDropdownItem(target, "timmy", "gold");
assert.containsOnce(target, ".o_tag");
assert.strictEqual(
target.querySelector(".o_tag").textContent,
"",
"The tag is displayed, but the web read is not finished yet"
);
assert.verifySteps(["name_search", "web_read"]);
await clickSave(target);
assert.doesNotHaveClass(target, "[name='timmy']", "o_field_invalid");
assert.verifySteps([]);
def.resolve();
await nextTick();
assert.strictEqual(target.querySelector(".o_tag").textContent, "gold");
assert.verifySteps(["web_save"]);
});
QUnit.test("Many2ManyTagsField with option 'no_quick_create' set to true", async (assert) => {
serverData.views = {
"partner_type,false,form": `<form><field name="name"/><field name="color"/></form>`,
@ -1767,7 +1892,7 @@ QUnit.module("Fields", (hooks) => {
arch: `<form><field name="timmy" widget="many2many_tags" context="{ 'append_coucou': True }"/></form>`,
async mockRPC(route, args, performRPC) {
const result = await performRPC(route, args);
if (args.method === "read") {
if (args.method === "web_read") {
if (args.kwargs.context.append_coucou) {
assert.step("read with context given");
result[0].display_name += " coucou";
@ -1799,7 +1924,7 @@ QUnit.module("Fields", (hooks) => {
arch: `<list editable="top"><field name="timmy" widget="many2many_tags" context="{ 'append_coucou': True }"/></list>`,
async mockRPC(route, args, performRPC) {
const result = await performRPC(route, args);
if (args.method === "read") {
if (args.method === "web_read") {
if (args.kwargs.context.append_coucou) {
assert.step("read with context given");
result[0].display_name += " coucou";
@ -1823,4 +1948,89 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps(["name search with context given", "read with context given"]);
assert.strictEqual(target.querySelector(".o_field_tags").innerText, "gold coucou");
});
QUnit.test("Many2ManyTagsField doesn't use virtualId for 'name_search'", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `<form>
<field name="turtles" widget="many2many_tags"/>
<field name="turtles">
<tree>
<field name="display_name"/>
</tree>
<form>
<field name="display_name"/>
</form>
</field>
</form>`,
async mockRPC(route, { method, kwargs }) {
if (method === "name_search") {
assert.step("name_search");
// no virtualId in domain
assert.deepEqual(kwargs.args, ["!", ["id", "in", [2]]]);
}
},
});
await addRow(target);
assert.containsOnce(target, ".modal");
await editInput(target, ".modal [name='display_name'] input", "yop");
await click(target.querySelector(".modal .o_form_button_save"));
assert.containsNone(target, ".modal");
assert.deepEqual(
[...target.querySelectorAll("[name='turtles'] .o_tag_badge_text")].map(
(el) => el.textContent
),
["donatello", "yop"]
);
assert.deepEqual(
[...target.querySelectorAll("[name='turtles'] .o_data_row")].map(
(el) => el.textContent
),
["donatello", "yop"]
);
await click(target.querySelector("[name='turtles'] input"));
assert.verifySteps(["name_search"]);
});
QUnit.test(
"Many2ManyTagsField: quickly remove several tags with backspace",
async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner.onchanges.timmy = () => {};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags"/>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.step(`onchange ${JSON.stringify(args.args[1].timmy)}`);
return def;
}
},
resId: 1,
});
assert.containsN(target, ".o_field_many2many_tags .badge", 2);
target.querySelectorAll(".o_field_many2many_tags .badge")[1].focus();
triggerHotkey("BackSpace");
triggerHotkey("BackSpace");
def.resolve();
await nextTick();
assert.containsN(target, ".o_field_many2many_tags .badge", 1);
assert.verifySteps(["onchange [[3,14]]"]);
}
);
});

View file

@ -10,6 +10,7 @@ import {
selectDropdownItem,
triggerEvent,
clickDiscard,
clickOpenedDropdownItem,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { browser } from "@web/core/browser/browser";
@ -87,7 +88,7 @@ QUnit.module("Fields", (hooks) => {
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_field_many2one_avatar > div");
assert.containsOnce(target, ".o_input_dropdown");
assert.strictEqual(target.querySelector(".o_input_dropdown input").value, "Aline");
@ -186,7 +187,7 @@ QUnit.module("Fields", (hooks) => {
});
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id']")),
["Aline", "Christine", "Aline", ""]
);
const imgs = target.querySelectorAll(".o_m2o_avatar > img");
@ -204,7 +205,7 @@ QUnit.module("Fields", (hooks) => {
});
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id'] span span")),
getNodesTextContent(target.querySelectorAll(".o_data_cell[name='user_id']")),
["Aline", "Christine", "Aline", ""]
);
@ -226,8 +227,7 @@ QUnit.module("Fields", (hooks) => {
type: "form",
resModel: "partner",
serverData,
arch:
'<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
arch: '<form><field name="user_id" widget="many2one_avatar" placeholder="Placeholder"/></form>',
});
assert.strictEqual(
@ -255,7 +255,7 @@ QUnit.module("Fields", (hooks) => {
});
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"));
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id']"));
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
assert.verifySteps([]);
@ -280,7 +280,7 @@ QUnit.module("Fields", (hooks) => {
});
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"));
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id']"));
assert.hasClass(target.querySelector(".o_data_row"), "o_selected_row");
assert.verifySteps([]);
@ -304,12 +304,41 @@ QUnit.module("Fields", (hooks) => {
</tree>`,
});
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id'] span span"));
await click(target.querySelector(".o_data_row .o_data_cell [name='user_id']"));
assert.containsNone(target, ".o_selected_row");
assert.verifySteps(["openRecord"]);
});
QUnit.test(
"readonly many2one_avatar in form view should contain a link",
async function (assert) {
await makeView({
type: "form",
serverData,
resModel: "partner",
resId: 1,
arch: `<form><field name="user_id" widget="many2one_avatar" readonly="1"/></form>`,
});
assert.containsOnce(target, "[name='user_id'] a");
}
);
QUnit.test(
"readonly many2one_avatar in list view should not contain a link",
async function (assert) {
await makeView({
type: "list",
serverData,
resModel: "partner",
arch: `<tree><field name="user_id" widget="many2one_avatar"/></tree>`,
});
assert.containsNone(target, "[name='user_id'] a");
}
);
QUnit.test("cancelling create dialog should clear value in the field", async function (assert) {
serverData.views = {
"user,false,form": `
@ -332,11 +361,171 @@ QUnit.module("Fields", (hooks) => {
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 clickOpenedDropdownItem(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");
});
QUnit.test("widget many2one_avatar in kanban view (load more dialog)", async function (assert) {
assert.expect(1);
for (let id = 1; id <= 10; id++) {
serverData.models.user.records.push({
id,
display_name: `record ${id}`,
});
}
serverData.views = {
"user,false,list": '<tree><field name="display_name"/></tree>',
"user,false,search": "<search/>",
};
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_id" widget="many2one_avatar"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
});
// open popover
await click(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > a.o_quick_assign"
)
);
// load more
await click(
document.querySelector(".o-overlay-container .o_m2o_dropdown_option_search_more")
);
await click(document.querySelector(".o_dialog .o_list_table .o_data_row .o_data_cell"));
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/1/avatar_128",
"should have correct avatar image"
);
});
QUnit.test("widget many2one_avatar in kanban view", async function (assert) {
assert.expect(5);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_id" widget="many2one_avatar"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
});
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/17/avatar_128",
"should have correct avatar image"
);
assert.containsOnce(
target,
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
"should have the quick assign icon"
);
// open popover
await click(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign"
)
);
const popover = document.querySelector(".o-overlay-container");
assert.strictEqual(
document.activeElement,
popover.querySelector("input"),
"the input inside the popover should have the focus"
);
// select first input
await click(popover.querySelector(".o-autocomplete--dropdown-item"));
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/17/avatar_128",
"should have correct avatar image"
);
assert.containsNone(
target,
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
"should not have the quick assign icon"
);
});
QUnit.test(
"widget many2one_avatar in kanban view without access rights",
async function (assert) {
assert.expect(2);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban edit="0" create="0">
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="oe_kanban_footer">
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_right">
<field name="user_id" widget="many2one_avatar"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>`,
});
assert.strictEqual(
target.querySelector(
".o_kanban_record:nth-child(1) .o_field_many2one_avatar .o_m2o_avatar > img"
).dataset.src,
"/web/image/user/17/avatar_128",
"should have correct avatar image"
);
assert.containsNone(
target,
".o_kanban_record:nth-child(4) .o_field_many2one_avatar .o_m2o_avatar > .o_quick_assign",
"should not have the quick assign icon"
);
}
);
});

View file

@ -10,7 +10,6 @@ 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";
@ -133,8 +132,8 @@ QUnit.module("Fields", (hooks) => {
</form>
`,
async mockRPC(route, args, performRPC) {
if (args.method === CREATE && args.model === SALE_ORDER_LINE) {
const selectedId = args.args[0][PRODUCT_FIELD_NAME];
if (args.method === "web_save" && args.model === SALE_ORDER_LINE) {
const selectedId = args.args[1][PRODUCT_FIELD_NAME];
assert.equal(
selectedId,
selectedRecordTest.id,
@ -172,8 +171,8 @@ QUnit.module("Fields", (hooks) => {
<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];
if (args.method === "web_save" && args.model === SALE_ORDER_LINE) {
const selectedId = args.args[1][PRODUCT_FIELD_NAME];
assert.equal(
selectedId,
selectedRecordTest.id,

View file

@ -10,7 +10,7 @@ import {
patchWithCleanup,
triggerEvent,
} from "@web/../tests/helpers/utils";
import { session } from "@web/session";
import { currencies } from "@web/core/currency";
let serverData;
let target;
@ -252,15 +252,12 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("with currency digits != 2 - float field", async function (assert) {
// need to also add it to the session (as currencies are loaded there)
patchWithCleanup(session, {
currencies: {
...session.currencies,
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
patchWithCleanup(currencies, {
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
});
@ -313,15 +310,12 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("with currency digits != 2 - monetary field", async function (assert) {
// need to also add it to the session (as currencies are loaded there)
patchWithCleanup(session, {
currencies: {
...session.currencies,
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
patchWithCleanup(currencies, {
3: {
name: "VEF",
symbol: "Bs.F",
position: "after",
digits: [0, 4],
},
});
@ -406,7 +400,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
@ -501,7 +495,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
@ -685,7 +679,7 @@ QUnit.module("Fields", (hooks) => {
string: "m2m",
type: "many2many",
relation: "partner",
default: [[6, false, [2]]],
default: [[4, 2]],
};
serverData.views = {
"partner,false,list": `
@ -777,7 +771,11 @@ QUnit.module("Fields", (hooks) => {
</tree>`,
});
await click(target.querySelector(".o_list_button_add"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
)
);
assert.containsOnce(
target,
".o_selected_row .o_field_widget[name=float_field] input",
@ -865,15 +863,12 @@ QUnit.module("Fields", (hooks) => {
},
];
patchWithCleanup(session, {
currencies: {
...session.currencies,
1: {
name: "USD",
symbol: "$",
position: "before",
digits: [0, 4],
},
patchWithCleanup(currencies, {
1: {
name: "USD",
symbol: "$",
position: "before",
digits: [0, 4],
},
});

View file

@ -2,13 +2,12 @@
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { registry } from "@web/core/registry";
import { getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { getFixture, nextTick, patchWithCleanup, triggerEvent } 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;
import { Component, mount, useState, xml } from "@odoo/owl";
let serverData;
let target;
@ -358,4 +357,18 @@ QUnit.module("Fields", (hooks) => {
await testInputElements(target.querySelectorAll("main > input"));
}
);
QUnit.test("select all content on focus", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `<form><field name="monetary"/></form>`,
});
const input = target.querySelector(".o_field_widget[name='monetary'] input");
await triggerEvent(input, null, "focus");
assert.strictEqual(input.selectionStart, 0);
assert.strictEqual(input.selectionEnd, 4);
});
});

View file

@ -83,8 +83,8 @@ QUnit.module("Fields", (hooks) => {
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") });
if (method === "web_save") {
assert.deepEqual(args[1], { document: btoa("test") });
}
},
});

View file

@ -27,15 +27,16 @@ QUnit.module("Fields", (hooks) => {
searchable: true,
},
float_field: {
string: "Float_field",
string: "float_field",
type: "float",
digits: [0, 1],
},
sortable: true,
searchable: true,
},
},
records: [
{ id: 1, foo: "yop", int_field: 10 },
{ id: 2, foo: "gnap", int_field: 80 },
{ id: 3, foo: "dop", float_field: 65.6},
{ id: 3, foo: "blip", float_field: 33.3333 },
],
onchanges: {},
},
@ -69,74 +70,17 @@ QUnit.module("Fields", (hooks) => {
"should have a pie chart"
);
assert.strictEqual(
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie .o_pie_value")
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .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"
target
.querySelector(".o_field_percent_pie.o_field_widget .o_pie")
.style.background.replaceAll(/\s+/g, " "),
"conic-gradient( var(--PercentPieField-color-active) 0% 10%, var(--PercentPieField-color-static) 0% 100% )",
"pie should have a background computed for its value of 10%"
);
});
@ -162,69 +106,17 @@ QUnit.module("Fields", (hooks) => {
"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")
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .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%"
target
.querySelector(".o_field_percent_pie.o_field_widget .o_pie")
.style.background.replaceAll(/\s+/g, " "),
"conic-gradient( var(--PercentPieField-color-active) 0% 80%, var(--PercentPieField-color-static) 0% 100% )",
"pie should have a background computed for its value of 80%"
);
});
@ -243,60 +135,72 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 3,
});
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")
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_value")
.textContent,
"66%",
"should have 66% as pie value since float_field=65.6"
"33.33%",
"should have 33.33% as pie value since float_field=33.3333 and its value is rounded to 2 decimals"
);
assert.strictEqual(
target
.querySelector(".o_field_percent_pie.o_field_widget .o_pie")
.style.background.replaceAll(/\s+/g, " "),
"conic-gradient( var(--PercentPieField-color-active) 0% 33.3333%, var(--PercentPieField-color-static) 0% 100% )",
"pie should have a background computed for its value of 33.3333%"
);
});
// 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();
// });
QUnit.test(
"hide the string when the PercentPieField widget is used in the view",
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");
assert.isNotVisible(
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text")
);
}
);
QUnit.test(
"show the string when the PercentPieField widget is used in a button with the class oe_stat_button",
async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<div name="button_box" class="oe_button_box">
<button type="object" class="oe_stat_button">
<field name="int_field" widget="percentpie"/>
</button>
</div>
</form>`,
resId: 2,
});
assert.containsOnce(target, ".o_field_percent_pie.o_field_widget .o_pie");
assert.isVisible(
target.querySelector(".o_field_percent_pie.o_field_widget .o_pie_info .o_pie_text")
);
}
);
});

View file

@ -41,7 +41,7 @@ QUnit.module("Fields", (hooks) => {
<field name="float_field" widget="percentage"/>
</form>`,
mockRPC(route, { args, method }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].float_field,
0.24,

View file

@ -140,7 +140,11 @@ QUnit.module("Fields", (hooks) => {
await editInput(cell, "input", "new");
// save
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
assert.doesNotHaveClass(
cell.parentElement,
@ -260,4 +264,49 @@ QUnit.module("Fields", (hooks) => {
);
assert.hasAttrValue(phone, "href", "tel:+12345678900", "href should not contain any space");
});
QUnit.test(
"New record, fill in phone field, then click on call icon and save",
async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<sheet>
<group>
<field name="display_name" required="1"/>
<field name="foo" widget="phone"/>
</group>
</sheet>
</form>`,
});
await editInput(target, "div[name='display_name'] input[type='text']", "TEST");
await editInput(target, "div[name='foo'] input[type='tel']", "+12345678900");
target.querySelector(".o_field_widget[name=foo] input").focus();
await click(target.querySelector(".o_phone_form_link"));
assert.doesNotHaveClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible",
"save button should be visible"
);
await clickSave(target);
assert.deepEqual(
target.querySelector(".o_field_widget[name=display_name] input").value,
"TEST"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo] input").value,
"+12345678900"
);
assert.hasClass(
target.querySelector(".o_form_status_indicator_buttons"),
"invisible",
"save button should be invisible"
);
}
);
});

View file

@ -333,20 +333,24 @@ QUnit.module("Fields", (hooks) => {
</templates>
</kanban>`,
mockRPC(route, args) {
if (args.method === "write") {
assert.step(`write ${JSON.stringify(args.args)}`);
if (args.method === "web_save") {
assert.step(`web_save ${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"}]']);
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"));
assert.verifySteps(['web_save [[1],{"selection":"1"}]']);
assert.containsOnce(target, ".o_kanban_record .fa-star");
await click(target, ".o-kanban-button-new");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o-kanban-button-new"
);
await nextTick();
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"}]']);
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"));
assert.verifySteps(['web_save [[6],{"selection":"1"}]']);
assert.containsN(target, ".o_kanban_record .fa-star", 2);
});
@ -404,7 +408,10 @@ QUnit.module("Fields", (hooks) => {
);
// save
await click(target, ".o_list_button_save");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
);
assert.containsN(
target.querySelectorAll(".o_data_row")[0],
@ -514,7 +521,10 @@ QUnit.module("Fields", (hooks) => {
);
// save
await click(target, ".o_list_button_save");
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
);
rows = target.querySelectorAll(".o_data_row");
assert.containsN(
@ -627,8 +637,8 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
@ -637,7 +647,7 @@ QUnit.module("Fields", (hooks) => {
".o_field_widget .o_priority a.o_priority_star.fa-star-o"
);
await click(stars[stars.length - 1]);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
QUnit.test("PriorityField - prevent auto save with autosave option", async function (assert) {
@ -667,5 +677,4 @@ QUnit.module("Fields", (hooks) => {
await click(stars[stars.length - 1]);
assert.verifySteps([]);
});
});

View file

@ -1,9 +1,6 @@
/** @odoo-module **/
import {
makeFakeLocalizationService,
makeFakeNotificationService,
} from "@web/../tests/helpers/mock_services";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import {
click,
clickSave,
@ -87,7 +84,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.deepEqual(
args[1],
{ int_field: 999, float_field: 5, display_name: "new name" },
@ -129,7 +126,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].int_field,
69,
@ -181,7 +178,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].int_field,
69,
@ -228,7 +225,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].float_field,
69,
@ -256,6 +253,7 @@ QUnit.module("Fields", (hooks) => {
);
await editInput(target, ".o_progressbar_value .o_input", "69");
target.querySelector(".o_progressbar_value .o_input").blur(); // because clickSave does not trigger blur
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_progressbar").textContent +
@ -296,7 +294,7 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps([
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
"/web/dataset/call_kw/partner/web_read",
]);
});
@ -322,7 +320,7 @@ QUnit.module("Fields", (hooks) => {
</kanban>`,
resId: 1,
mockRPC(route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(args[1].int_field, 69, "New value of progress bar saved");
}
},
@ -370,18 +368,18 @@ QUnit.module("Fields", (hooks) => {
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>`,
<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") {
if (method === "web_save") {
throw new Error("Not supposed to write");
}
},
@ -465,7 +463,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC: function (route, { method, args }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].int_field,
1037,
@ -493,6 +491,7 @@ QUnit.module("Fields", (hooks) => {
assert.ok(target.querySelector(".o_form_view .o_form_editable"), "Form in edit mode");
await editInput(target, ".o_field_widget input", "1#037:9");
target.querySelector(".o_progressbar_value .o_input").blur(); // because clickSave does not trigger blur
await clickSave(target);
assert.strictEqual(
@ -507,13 +506,6 @@ QUnit.module("Fields", (hooks) => {
"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,
@ -534,8 +526,39 @@ QUnit.module("Fields", (hooks) => {
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");
assert.containsOnce(
target,
".o_form_status_indicator span.text-danger",
"The form has not been saved"
);
assert.strictEqual(
target.querySelector(".o_form_button_save").disabled,
true,
"save button is disabled"
);
}
);
QUnit.test(
"ProgressBarField: color is correctly set when value > max value",
async function (assert) {
serverData.models.partner.records[0].float_field = 101;
await makeView({
serverData,
type: "form",
resModel: "partner",
arch: `
<form>
<field name="float_field" widget="progressbar" options="{'overflow_class': 'bg-warning'}"/>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_progressbar .bg-warning",
"As the value has excedded the max value, the color should be set to bg-warning"
);
}
);
});

View file

@ -1,6 +1,6 @@
/** @odoo-module **/
import { click, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
import { click, clickSave, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -86,11 +86,19 @@ QUnit.module("Fields", (hooks) => {
target.querySelector(".o_field_radio").textContent.replace(/\s+/g, ""),
"xphonexpad"
);
assert.containsNone(target, "input:checked", "none of the input should be checked");
assert.containsNone(
target,
"input.o_radio_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");
assert.containsOnce(
target,
"input.o_radio_input:checked",
"one of the input should be checked"
);
await clickSave(target);
@ -148,7 +156,7 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, "input[type='checkbox']");
await click(target, ".o_field_boolean input[type='checkbox']");
assert.containsOnce(
target,
@ -161,7 +169,7 @@ QUnit.module("Fields", (hooks) => {
"the other of the input should be checked"
);
await click(target, "input[type='checkbox']");
await click(target, ".o_field_boolean input[type='checkbox']");
assert.containsOnce(
target,
"input.o_radio_input[data-value='41']:checked",
@ -205,6 +213,52 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Two RadioField with same selection", async function (assert) {
serverData.models.partner.fields.color_2 = serverData.models.partner.fields.color;
serverData.models.partner.records[0].color = "black";
serverData.models.partner.records[0].color_2 = "black";
await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 1,
arch: `
<form>
<group>
<field name="color" widget="radio"/>
</group>
<group>
<field name="color_2" widget="radio"/>
</group>
</form>`,
});
assert.hasAttrValue(
target.querySelector("[name='color'] input.o_radio_input:checked"),
"data-value",
"black"
);
assert.hasAttrValue(
target.querySelector("[name='color_2'] input.o_radio_input:checked"),
"data-value",
"black"
);
// click on Red
await click(target.querySelector("[name='color_2'] label"));
assert.hasAttrValue(
target.querySelector("[name='color'] input.o_radio_input:checked"),
"data-value",
"black"
);
assert.hasAttrValue(
target.querySelector("[name='color_2'] input.o_radio_input:checked"),
"data-value",
"red"
);
});
QUnit.test("fieldradio widget has o_horizontal or o_vertical class", async function (assert) {
serverData.models.partner.fields.color2 = serverData.models.partner.fields.color;
@ -261,7 +315,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="selection" widget="radio"/></form>',
mockRPC: function (route, { args, method, model }) {
if (model === "partner" && method === "write") {
if (model === "partner" && method === "web_save") {
assert.strictEqual(args[1].selection, "1", "should write correct value");
}
},
@ -288,61 +342,6 @@ QUnit.module("Fields", (hooks) => {
);
});
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",

View file

@ -12,9 +12,9 @@ import {
clickSave,
triggerHotkey,
nextTick,
makeDeferred,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { makeView, makeViewInDialog, setupViewRegistries } from "@web/../tests/views/helpers";
let target;
let serverData;
@ -252,9 +252,7 @@ QUnit.module("Fields", (hooks) => {
"name_search", // for the select
"name_search", // for the spawned many2one
"name_create",
"create",
"read",
"name_get",
"web_save",
],
"The name_create method should have been called"
);
@ -408,7 +406,7 @@ QUnit.module("Fields", (hooks) => {
patchWithCleanup(actionService, {
start() {
const service = this._super(...arguments);
const service = super.start(...arguments);
return {
...service,
doAction(action) {
@ -422,7 +420,7 @@ QUnit.module("Fields", (hooks) => {
},
});
await makeView({
await makeViewInDialog({
type: "form",
resModel: "partner",
resId: 1,
@ -431,7 +429,7 @@ QUnit.module("Fields", (hooks) => {
<form>
<sheet>
<group>
<field name="reference" string="custom label" open_target="new" />
<field name="reference" string="custom label"/>
</group>
</sheet>
</form>`,
@ -460,7 +458,7 @@ QUnit.module("Fields", (hooks) => {
"the name_search should be done on the newly set model"
);
}
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(model, "partner", "should write on the current model");
assert.deepEqual(
args,
@ -502,11 +500,13 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_external_button");
assert.strictEqual(
target.querySelector(".modal .modal-title").textContent.trim(),
target
.querySelector(".o_dialog:not(.o_inactive_modal) .modal-title")
.textContent.trim(),
"Open: custom label",
"dialog title should display the custom string label"
);
await click(target, ".modal .o_form_button_cancel");
await click(target, ".o_dialog:not(.o_inactive_modal) .o_form_button_cancel");
await editSelect(target, ".o_field_widget select", "partner_type");
assert.strictEqual(
@ -526,16 +526,10 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("Many2One 'Search More...' updates on resModel change", async function (assert) {
// Patch the Many2XAutocomplete default search limit options
patchWithCleanup(Many2XAutocomplete.defaultProps, {
searchLimit: -1,
});
QUnit.test("Many2One 'Search more...' updates on resModel change", async function (assert) {
serverData.views = {
"product,false,list": '<tree><field name="display_name"/></tree>',
"product,false,search": '<search/>',
"product,false,search": "<search/>",
};
await makeView({
@ -546,14 +540,25 @@ QUnit.module("Fields", (hooks) => {
});
// Selecting a relation
await editSelect(target.querySelector("div.o_field_reference"), "select.o_input", "partner_type");
await editSelect(
target.querySelector("div.o_field_reference"),
"select.o_input",
"partner_type"
);
// Selecting another relation
await editSelect(target.querySelector("div.o_field_reference"), "select.o_input", "product");
await editSelect(
target.querySelector("div.o_field_reference"),
"select.o_input",
"product"
);
// Opening the Search More... option
await click(target.querySelector("div.o_field_reference"), "input.o_input");
await click(target.querySelector("div.o_field_reference"), ".o_m2o_dropdown_option_search_more");
await click(
target.querySelector("div.o_field_reference"),
".o_m2o_dropdown_option_search_more"
);
assert.strictEqual(
target.querySelector("div.modal td.o_data_cell").innerText,
@ -562,82 +567,44 @@ QUnit.module("Fields", (hooks) => {
);
});
QUnit.test("computed reference field changed by onchange to 'False,0' value", async function (assert) {
assert.expect(1);
QUnit.test(
"computed reference field changed by onchange to 'False,0' value",
async function (assert) {
assert.expect(1);
serverData.models.partner.onchanges = {
bar(obj) {
if (!obj.bar) {
obj.reference_char = "False,0";
}
},
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
serverData.models.partner.onchanges = {
bar(obj) {
if (!obj.bar) {
obj.reference_char = "False,0";
}
},
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="bar"/>
<field name="reference_char" widget="reference"/>
</form>`,
mockRPC(route, { args, method }) {
if (method === "create") {
assert.deepEqual(args[0], {
bar: false,
reference_char: "False,0",
});
}
},
});
mockRPC(route, { args, method }) {
if (method === "web_save") {
assert.deepEqual(args[1], {
bar: false,
reference_char: "False,0",
});
}
},
});
// trigger the onchange to set a value for the reference field
await click(target, ".o_field_boolean input");
// trigger the onchange to set a value for the reference field
await click(target, ".o_field_boolean input");
// save
await clickSave(target);
});
QUnit.test("ReferenceField with model field", async function (assert) {
serverData.models.partner.onchanges = {
color(obj) {
if (obj.color === "black") {
obj.model_id = 20;
obj.reference = "product,37";
} else {
obj.model_id = 17;
obj.reference = "partner,1";
}
},
};
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="color" />
<field name="model_id" invisible="1"/>
<field name="reference" options="{'model_field': 'model_id'}" />
</form>`,
mockRPC(route, { args, method }) {
if (method === "write") {
assert.step("write");
assert.strictEqual(args[1].reference, "partner,4");
}
},
});
await editSelect(target, "select", '"black"');
await editSelect(target, "select", '"red"');
await editInput(target, ".o_field_widget[name=reference] input", "aaa");
await click(target, ".ui-autocomplete .ui-menu-item:first-child");
await clickSave(target);
assert.verifySteps(["write"]);
});
// save
await clickSave(target);
}
);
QUnit.test("interact with reference field changed by onchange", async function (assert) {
assert.expect(2);
@ -659,8 +626,8 @@ QUnit.module("Fields", (hooks) => {
<field name="reference"/>
</form>`,
mockRPC(route, { args, method }) {
if (method === "create") {
assert.deepEqual(args[0], {
if (method === "web_save") {
assert.deepEqual(args[1], {
bar: false,
reference: "partner,4",
});
@ -707,14 +674,8 @@ QUnit.module("Fields", (hooks) => {
</group>
</sheet>
</form>`,
mockRPC(route, { method, model }) {
if (method === "name_get") {
assert.step(model);
}
},
});
assert.verifySteps(["product"], "the first name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name='reference'] select").value,
"product",
@ -729,7 +690,6 @@ QUnit.module("Fields", (hooks) => {
// trigger onchange
await editInput(target, ".o_field_widget[name=int_field] input", 12);
assert.verifySteps(["partner_type"], "the second name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name='reference'] select").value,
"partner_type",
@ -784,7 +744,6 @@ QUnit.module("Fields", (hooks) => {
obj.foo = "product," + obj.int_field;
},
};
let nbNameGet = 0;
await makeView({
type: "form",
@ -800,23 +759,26 @@ QUnit.module("Fields", (hooks) => {
</group>
</sheet>
</form>`,
mockRPC(route, { model, method }) {
if (model === "product" && method === "name_get") {
mockRPC(route, { model, method, args }) {
if (
model === "product" &&
method === "read" &&
args[1].length === 1 &&
args[1][0] === "display_name"
) {
nbNameGet++;
}
},
});
assert.strictEqual(nbNameGet, 1, "the first name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo]").textContent,
"xphone",
"foo field should be correctly set"
);
// trigger onchange
await editInput(target, ".o_field_widget[name=int_field] input", 41);
await nextTick();
assert.strictEqual(nbNameGet, 2, "the second name_get should have been done");
assert.strictEqual(
target.querySelector(".o_field_widget[name=foo]").textContent,
@ -869,7 +831,6 @@ QUnit.module("Fields", (hooks) => {
<field name="reference" options="{'model_field': 'model_id'}" />
</form>`,
});
assert.containsNone(
target,
"select",
@ -891,6 +852,7 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name='model_id'] input", "Partner");
await click(target, ".ui-autocomplete .ui-menu-item:first-child");
await nextTick();
assert.strictEqual(
target.querySelector(".o_field_widget[name='reference'] input").value,
"",
@ -946,7 +908,7 @@ QUnit.module("Fields", (hooks) => {
);
QUnit.test("Reference field with default value in list view", async function (assert) {
assert.expect(2);
assert.expect(1);
await makeView({
type: "list",
@ -960,19 +922,29 @@ QUnit.module("Fields", (hooks) => {
mockRPC: (route, { method, args }) => {
if (method === "onchange") {
return {
value: {reference: "partner,2"},
value: {
reference: {
id: { id: 2, model: "partner" },
display_name: "second record",
},
},
};
} else if (method === "create") {
assert.strictEqual(args.length, 1);
assert.strictEqual(args[0].reference, "partner,2");
} else if (method === "web_save") {
assert.strictEqual(args[1].reference, "partner,2");
}
},
});
await click(target, '.o_list_button_add');
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
);
await click(target, '.o_list_char[name="display_name"] input');
await editInput(target, '.o_list_char[name="display_name"] input', "Blabla");
await click(target, '.o_list_button_save');
await click(
target,
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
);
});
QUnit.test(
@ -1003,6 +975,8 @@ QUnit.module("Fields", (hooks) => {
// Select the second product without changing the model
await click(target, ".o_list_table .reference_field");
await nextTick();
await click(target, ".o_list_table .reference_field input");
// Enter to select it
@ -1051,6 +1025,7 @@ QUnit.module("Fields", (hooks) => {
);
await click(target.querySelector(".o_list_table .o_data_cell"));
await nextTick();
await editInput(target, ".o_list_table [name='name'] input", "plop");
await click(target, ".o_form_view");
assert.strictEqual(
@ -1093,8 +1068,13 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_list_table td.o_list_many2one");
await click(target, ".o_list_table .o_list_many2one input");
//Select the "Partner" option, different from original "Product"
const dropdownItems = [...target.querySelectorAll(".o_list_table .o_list_many2one .o_input_dropdown .dropdown-item")];
await click(dropdownItems.filter(item => item.text === "Partner")[0]);
const dropdownItems = [
...target.querySelectorAll(
".o_list_table .o_list_many2one .o_input_dropdown .dropdown-item"
),
];
await click(dropdownItems.filter((item) => item.text === "Partner")[0]);
await nextTick();
assert.strictEqual(target.querySelector(".reference_field input").value, "");
assert.strictEqual(target.querySelector(".o_list_many2one input").value, "Partner");
//Void the associated, required, "reference" field and make sure the form marks the field as required
@ -1158,4 +1138,56 @@ QUnit.module("Fields", (hooks) => {
"the selection list of the reference field should exist when hide_model=False and no model_field specified."
);
});
QUnit.test("reference field should await fetch model before render", async function (assert) {
serverData.models.partner.records[0].model_id = 20;
const def = makeDeferred();
makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="model_id" invisible="1"/>
<field name="reference" options="{'model_field': 'model_id'}" />
</form>`,
async mockRPC(route, args) {
if (args.method === "read" && args.model === "ir.model") {
await def;
}
},
});
await nextTick();
await nextTick();
assert.containsNone(target, ".o_form_view");
def.resolve();
await nextTick();
assert.containsOnce(target, ".o_form_view");
});
QUnit.test("reference char with list view pager navigation", async function (assert) {
assert.expect(2);
serverData.models.partner.records[0].reference_char = "product,37";
serverData.models.partner.records[1].reference_char = "product,41";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form edit="0"><field name="reference_char" widget="reference" string="Record"/></form>`,
resIds: [1, 2],
});
assert.strictEqual(
target.querySelector(".o_field_reference .o_form_uri").textContent,
"xphone"
);
await click(target, ".o_pager_next");
assert.strictEqual(
target.querySelector(".o_field_reference .o_form_uri").textContent,
"xpad"
);
});
});

View file

@ -8,6 +8,7 @@ import {
patchTimeZone,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { getPickerCell } from "../../core/datetime/datetime_test_helpers";
let serverData;
let target;
@ -116,22 +117,18 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.containsOnce(target, ".o_field_remaining_days input");
await editInput(target, ".o_datepicker_input", "10/10/2017");
await editInput(target, ".o_field_remaining_days input", "10/10/2017");
await click(target);
assert.containsOnce(document.body, ".modal");
assert.containsOnce(target, ".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");
await click(target, ".modal .modal-footer .btn-primary");
assert.strictEqual(
rows[0].querySelector(".o_data_cell").textContent,
@ -177,15 +174,11 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.containsOnce(target, ".o_field_remaining_days input");
await editInput(target, ".o_datepicker_input", "blabla");
await editInput(target, ".o_field_remaining_days input", "blabla");
await click(target);
assert.containsNone(document.body, ".modal");
assert.containsNone(target, ".modal");
assert.strictEqual(cells[0].textContent, "Today");
assert.strictEqual(cells[1].textContent, "Tomorrow");
}
@ -211,16 +204,12 @@ QUnit.module("Fields", (hooks) => {
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");
assert.containsOnce(target, "div.o_field_widget[name='date'] input");
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_remaining_days input");
assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
await click(getPickerCell("9").at(0));
await click(target, ".o_form_button_save");
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/09/2017");
});
@ -238,12 +227,9 @@ QUnit.module("Fields", (hooks) => {
</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");
assert.containsOnce(target, ".o_form_editable .o_field_widget[name='date'] input");
await click(target, ".o_field_widget[name='date'] input");
assert.containsOnce(target, ".o_datetime_picker");
}
);
@ -303,17 +289,12 @@ QUnit.module("Fields", (hooks) => {
target.querySelector(".o_field_widget input").value,
"10/08/2017 11:00:00"
);
assert.containsOnce(target, "div.o_field_widget[name='datetime'] .o_datepicker");
assert.containsOnce(target, "div.o_field_widget[name='datetime'] input");
await click(target.querySelector(".o_datepicker .o_datepicker_input"));
assert.containsOnce(
document.body,
".bootstrap-datetimepicker-widget",
"datepicker should be opened"
);
await click(target, ".o_field_widget input");
assert.containsOnce(target, ".o_datetime_picker", "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(getPickerCell("9").at(0));
await click(target, ".o_form_button_save");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,

View file

@ -1,6 +1,6 @@
/** @odoo-module **/
import { click, editSelect, editInput, getFixture, clickSave } from "@web/../tests/helpers/utils";
import { click, clickSave, editSelect, getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
@ -147,7 +147,7 @@ QUnit.module("Fields", (hooks) => {
"should have correct value in color field"
);
assert.verifySteps(["get_views", "read", "name_search", "name_search", "onchange"]);
assert.verifySteps(["get_views", "web_read", "name_search", "name_search", "onchange"]);
});
QUnit.test("unset selection field with 0 as key", async function (assert) {
@ -225,7 +225,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: '<form><field name="trululu" widget="selection" /></form>',
mockRPC(route, { args, method }) {
if (method === "write") {
if (method === "web_save") {
assert.strictEqual(
args[1].trululu,
false,
@ -257,59 +257,6 @@ QUnit.module("Fields", (hooks) => {
);
});
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",
@ -329,7 +276,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
<field name="color" required="feedback_value == 'bad'" />
</form>`,
});
@ -382,7 +329,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
<field name="color" required="feedback_value == 'bad'" />
</form>`,
});
@ -421,4 +368,189 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(placeholderOption.textContent, "Placeholder");
assert.strictEqual(placeholderOption.value, "false");
});
QUnit.test("SelectionField in kanban view", async function (assert) {
assert.expect(3);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>`,
domain: [["id", "=", 1]],
});
assert.containsOnce(
target,
".o_field_widget[name='color'] select",
"SelectionKanbanField widget applied to selection field"
);
assert.containsN(
target.querySelector(".o_field_widget[name='color']"),
"option",
3,
"Three options are displayed (one blank option)"
);
assert.deepEqual(
[...target.querySelectorAll(".o_field_widget[name='color'] option")].map(
(option) => option.value
),
["false", '"red"', '"black"']
);
});
QUnit.test("SelectionField - auto save record in kanban view", async function (assert) {
assert.expect(2);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>`,
domain: [["id", "=", 1]],
mockRPC(_route, { method }) {
if (method === "web_save") {
assert.step("web_save");
}
},
});
await editSelect(target, ".o_field_widget[name='color'] select", '"black"');
assert.verifySteps(["web_save"]);
});
QUnit.test(
"SelectionField don't open form view on click in kanban view",
async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>`,
domain: [["id", "=", 1]],
selectRecord: () => {
assert.step("selectRecord");
},
});
await click(target, ".o_field_widget[name='color'] select");
assert.verifySteps([]);
}
);
QUnit.test("SelectionField is disabled if field readonly", async function (assert) {
assert.expect(1);
serverData.models.partner.fields.color.readonly = true;
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" />
</div>
</t>
</templates>
</kanban>
`,
domain: [["id", "=", 1]],
});
assert.containsOnce(
target,
".o_field_widget[name='color'] span",
"field should be readonly"
);
});
QUnit.test("SelectionField is disabled with a readonly attribute", async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection" readonly="1" />
</div>
</t>
</templates>
</kanban>
`,
domain: [["id", "=", 1]],
});
assert.containsOnce(
target,
".o_field_widget[name='color'] span",
"field should be readonly"
);
});
QUnit.test("SelectionField in kanban view with handle widget", async function (assert) {
// When records are draggable, most pointerdown events are default prevented. This test
// comes with a fix that blacklists "select" elements, i.e. pointerdown events on such
// elements aren't default prevented, because if they were, the select element can't be
// opened. The test is a bit artificial but there's no other way to test the scenario, as
// using editSelect simply triggers a "change" event, which obviously always works.
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<field name="int_field" widget="handle"/>
<templates>
<t t-name="kanban-box">
<div>
<field name="color" widget="selection"/>
</div>
</t>
</templates>
</kanban>`,
});
const ev = new PointerEvent("pointerdown", { bubbles: true, cancelable: true });
const select = target.querySelector(".o_kanban_record .o_field_widget[name=color] select");
select.dispatchEvent(ev);
assert.notOk(ev.defaultPrevented);
});
});

View file

@ -4,8 +4,11 @@ import {
clickSave,
editInput,
getFixture,
makeDeferred,
nextTick,
patchWithCleanup,
triggerEvent,
triggerEvents,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { NameAndSignature } from "@web/core/signature/name_and_signature";
@ -60,10 +63,67 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("Signature Field");
QUnit.test("signature can be drawn", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form>
<field name="sign" widget="signature" />
</form>`,
mockRPC: async (route) => {
if (route === "/web/sign/get_fonts/") {
return {};
}
},
});
assert.containsNone(target, "div[name=sign] img.o_signature");
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 .o_web_sign_name_and_signature");
assert.containsNone(target, ".modal .btn.btn-primary:not([disabled])");
// Use a drag&drop simulation to draw a signature
const def = makeDeferred();
const jSignatureEl = target.querySelector(".modal .o_web_sign_signature");
$(jSignatureEl).on("change", def.resolve);
const { x, y, width, height } = target
.querySelector("canvas.jSignature")
.getBoundingClientRect();
await triggerEvents(jSignatureEl, "canvas.jSignature", [
["mousedown", { clientX: x + 1, clientY: y + 1 }],
["mousemove", { clientX: x + width - 1, clientY: height + height - 1 }],
["mouseup", { clientX: x + width - 1, clientY: height + height - 1 }],
]);
await def; // makes sure the signature stroke is taken into account by jSignature
await nextTick(); // await owl rendering
assert.containsOnce(target, ".modal .btn.btn-primary:not([disabled])");
// Click on "Adopt and Sign" button
await click(target, ".modal .btn.btn-primary:not([disabled])");
assert.containsNone(target, ".modal");
// The signature widget should now display the signature img
assert.containsNone(target, "div[name=sign] div.o_signature svg");
assert.containsOnce(target, "div[name=sign] img.o_signature");
const signImgSrc = target.querySelector("div[name=sign] img.o_signature").dataset.src;
assert.notOk(signImgSrc.includes("placeholder"));
assert.ok(signImgSrc.startsWith("data:image/png;base64,"));
});
QUnit.test("Set simple field in 'full_name' node option", async function (assert) {
patchWithCleanup(NameAndSignature.prototype, {
setup() {
this._super.apply(this, arguments);
super.setup(...arguments);
assert.step(this.props.signature.name);
},
});
@ -96,13 +156,18 @@ QUnit.module("Fields", (hooks) => {
".modal .modal-body a.o_web_sign_auto_button",
'should open a modal with "Auto" button'
);
assert.hasClass(
target.querySelector(".o_web_sign_auto_button"),
"active",
"'Auto' panel is visible by default"
);
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);
super.setup(...arguments);
assert.step(this.props.signature.name);
},
});
@ -183,7 +248,7 @@ QUnit.module("Fields", (hooks) => {
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000, 1659695820000
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
@ -203,9 +268,9 @@ QUnit.module("Fields", (hooks) => {
if (route === "/web/sign/get_fonts/") {
return {};
}
if (method === "write") {
assert.step("write");
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
assert.step("web_save");
args[1].write_date = lastUpdates[index];
args[1].sign = "4 kb";
index++;
}
@ -216,7 +281,7 @@ QUnit.module("Fields", (hooks) => {
"1659688620000"
);
await click(target, ".o_field_signature img", true);
await click(target, ".o_field_signature img", { skipVisibilityCheck: true });
assert.containsOnce(target, ".modal canvas");
let canvas = target.querySelector(".modal canvas");
@ -244,13 +309,13 @@ QUnit.module("Fields", (hooks) => {
);
await clickSave(target);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659692220000"
);
await click(target, ".o_field_signature img", true);
await click(target, ".o_field_signature img", { skipVisibilityCheck: true });
assert.containsOnce(target, ".modal canvas");
canvas = target.querySelector(".modal canvas");
@ -272,7 +337,7 @@ QUnit.module("Fields", (hooks) => {
`data:image/png;base64,${MYB64_2}`
);
await clickSave(target);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
assert.strictEqual(
getUnique(target.querySelector(".o_field_signature img")),
"1659695820000"
@ -292,7 +357,7 @@ QUnit.module("Fields", (hooks) => {
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
rec.write_date = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000, 1659695820000
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
@ -309,9 +374,9 @@ QUnit.module("Fields", (hooks) => {
<field name="sign" widget="signature" />
</form>`,
mockRPC(route, { method, args }) {
if (method === "write") {
assert.step("write");
args[1].__last_update = lastUpdates[index];
if (method === "web_save") {
assert.step("web_save");
args[1].write_date = lastUpdates[index];
args[1].sign = "4 kb";
index++;
}
@ -332,6 +397,6 @@ QUnit.module("Fields", (hooks) => {
getUnique(target.querySelector(".o_field_signature img")),
"1659692220000"
);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
});

View file

@ -82,26 +82,34 @@ QUnit.module("Fields", (hooks) => {
".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"
);
assert.containsNone(target, ".o_content .dropdown-menu", "there should not be a dropdown");
// 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.containsOnce(
document.body,
".o_content .dropdown-menu",
"there should be a dropdown"
);
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three options in the dropdown"
);
assert.hasClass(
target.querySelector(".dropdown-menu .dropdown-item:nth-child(2)"),
"active",
"current value has a checkmark"
);
// 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");
await click(target.querySelector(".o_content .dropdown-menu .dropdown-item"));
assert.containsNone(
target,
".o_content .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",
@ -118,7 +126,11 @@ QUnit.module("Fields", (hooks) => {
"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_content .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",
@ -137,17 +149,21 @@ QUnit.module("Fields", (hooks) => {
// 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.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
3,
"there should be three 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");
await click(target, ".o_content .dropdown-menu .dropdown-item:last-child");
assert.containsNone(
target,
".o_content .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",
@ -163,7 +179,7 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelector(".o_form_button_save"));
assert.containsNone(
target,
".dropdown-menu",
".o_content .dropdown-menu",
"there should still not be a dropdown anymore"
);
assert.containsNone(
@ -193,6 +209,21 @@ QUnit.module("Fields", (hooks) => {
assert.isNotVisible(target.querySelector(".dropdown-menu"));
});
QUnit.test("StateSelectionField for form view with hide_label option", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="selection" widget="state_selection" options="{'hide_label': False}"/>
</form>
`,
resId: 1,
});
assert.containsOnce(target, ".o_status_label");
});
QUnit.test("StateSelectionField for list view with hide_label option", async function (assert) {
Object.assign(serverData.models.partner.fields, {
graph_type: {
@ -214,7 +245,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree>
<field name="graph_type" widget="state_selection" options="{'hide_label': True}"/>
<field name="selection" widget="state_selection"/>
<field name="selection" widget="state_selection" options="{'hide_label': False}"/>
</tree>`,
});
@ -281,7 +312,7 @@ QUnit.module("Fields", (hooks) => {
".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");
assert.containsNone(target, ".o_content .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");
@ -293,16 +324,16 @@ QUnit.module("Fields", (hooks) => {
"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.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three options in the dropdown"
);
// Click on the first option, "Normal"
await click(target.querySelector(".dropdown-menu .dropdown-item"));
await click(target.querySelector(".o_content .dropdown-menu .dropdown-item"));
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
@ -319,7 +350,7 @@ QUnit.module("Fields", (hooks) => {
".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, ".o_content .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
@ -342,24 +373,28 @@ QUnit.module("Fields", (hooks) => {
".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, ".o_content .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.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three 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");
assert.containsNone(
target,
".o_content .dropdown-menu",
"there should not be a dropdown anymore"
);
const firstCell = target.querySelector("tbody td.o_state_selection_cell");
assert.doesNotHaveClass(
firstCell.parentElement,
@ -378,17 +413,21 @@ QUnit.module("Fields", (hooks) => {
".o_state_selection_cell .o_field_state_selection span.o_status"
)[3]
);
assert.containsOnce(target, ".dropdown-menu", "there should be a dropdown");
assert.containsOnce(target, ".o_content .dropdown-menu", "there should be a dropdown");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
2,
"there should be two options in the dropdown"
".o_content .dropdown-menu .dropdown-item",
3,
"there should be three 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");
await click(target, ".o_content .dropdown-menu .dropdown-item:last-child");
assert.containsNone(
target,
".o_content .dropdown-menu",
"there should not be a dropdown anymore"
);
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
@ -406,10 +445,14 @@ QUnit.module("Fields", (hooks) => {
2,
"should now have two green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
assert.containsNone(target, ".o_content .dropdown-menu", "there should not be a dropdown");
// save
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
assert.containsN(
target,
".o_state_selection_cell .o_field_state_selection span.o_status",
@ -427,11 +470,11 @@ QUnit.module("Fields", (hooks) => {
2,
"should have two green status"
);
assert.containsNone(target, ".dropdown-menu", "there should not be a dropdown");
assert.containsNone(target, ".o_content .dropdown-menu", "there should not be a dropdown");
});
QUnit.test(
'StateSelectionField edited by the smart action "Set kanban state..."',
'StateSelectionField edited by the smart actions "Set kanban state as <state name>"',
async function (assert) {
await makeView({
type: "form",
@ -448,20 +491,23 @@ QUnit.module("Fields", (hooks) => {
triggerHotkey("control+k");
await nextTick();
const idx = [...target.querySelectorAll(".o_command")]
.map((el) => el.textContent)
.indexOf("Set kanban state...ALT + SHIFT + R");
var commandTexts = [...target.querySelectorAll(".o_command")].map(
(el) => el.textContent
);
assert.ok(commandTexts.includes("Set kanban state as NormalALT + D"));
const idx = commandTexts.indexOf("Set kanban state as DoneALT + G");
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");
triggerHotkey("control+k");
await nextTick();
commandTexts = [...target.querySelectorAll(".o_command")].map((el) => el.textContent);
assert.ok(commandTexts.includes("Set kanban state as NormalALT + D"));
assert.ok(commandTexts.includes("Set kanban state as BlockedALT + F"));
assert.notOk(commandTexts.includes("Set kanban state as DoneALT + G"));
}
);
@ -492,17 +538,17 @@ QUnit.module("Fields", (hooks) => {
});
await click(target, ".o_status");
let dropdownItemTexts = [...target.querySelectorAll(".dropdown-item")].map(
(el) => el.textContent
);
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom done"]);
let dropdownItemTexts = [
...target.querySelectorAll(".o_field_state_selection .dropdown-item"),
].map((el) => el.textContent);
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom blocked", "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"]);
dropdownItemTexts = [
...target.querySelectorAll(".o_field_state_selection .dropdown-item"),
].map((el) => el.textContent);
assert.deepEqual(dropdownItemTexts, ["Custom normal", "Custom blocked", "Custom done"]);
});
QUnit.test("works when required in a readonly view ", async function (assert) {
@ -523,18 +569,19 @@ QUnit.module("Fields", (hooks) => {
</templates>
</kanban>`,
mockRPC: (route, args, performRPC) => {
if (route === "/web/dataset/call_kw/partner/write") {
assert.step("write");
if (route === "/web/dataset/call_kw/partner/web_save") {
assert.step("web_save");
}
return performRPC(route, args);
},
});
assert.containsNone(target, ".o_status_label");
await click(target, ".o_field_state_selection button");
const doneItem = target.querySelectorAll(".dropdown-item")[1]; // item "done";
const doneItem = target.querySelectorAll(".dropdown-item")[2]; // item "done";
await click(doneItem);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
assert.hasClass(target.querySelector(".o_field_state_selection span"), "o_status_green");
});
@ -555,15 +602,15 @@ QUnit.module("Fields", (hooks) => {
</form>`,
resId: 1,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
await click(target, ".o_field_widget.o_field_state_selection .o_status");
await click(target, ".dropdown-menu .dropdown-item:last-child");
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
}
);
@ -595,4 +642,58 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps([]);
}
);
QUnit.test(
"StateSelectionField - hotkey handling when there are more than 3 options available",
async function (assert) {
serverData.models.partner.fields.selection.selection.push(
["martin", "Martin"],
["martine", "Martine"]
);
serverData.models.partner.records[0].selection = null;
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,
});
await click(target, ".o_field_widget.o_field_state_selection .o_status");
assert.containsN(
target,
".dropdown-menu .dropdown-item",
5,
"Five choices are displayed"
);
triggerHotkey("control+k");
await nextTick();
assert.strictEqual(
target.querySelector(".o_command#o_command_2").textContent,
"Set kanban state as DoneALT + G",
"hotkey and command are present"
);
assert.strictEqual(
target.querySelector(".o_command#o_command_4").textContent,
"Set kanban state as Martine",
"no hotkey is present, but the command exists"
);
await click(target.querySelector(".o_command#o_command_2"));
assert.hasClass(
target.querySelector(".o_status"),
"o_status_green",
"green color and Done state have been set"
);
}
);
});

View file

@ -1,18 +1,22 @@
/** @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,
clickSave,
editInput,
getFixture,
getNodesTextContent,
nextTick,
patchWithCleanup,
selectDropdownItem,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { EventBus } from "@odoo/owl";
@ -247,10 +251,7 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.ok(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)"
@ -263,10 +264,7 @@ QUnit.module("Fields", (hooks) => {
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"
);
assert.ok(target.querySelector(".o_statusbar_status button[data-value='1']").disabled);
});
QUnit.test("statusbar with no status", async function (assert) {
@ -285,9 +283,9 @@ QUnit.module("Fields", (hooks) => {
});
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
assert.strictEqual(
target.querySelector(".o_statusbar_status").children.length,
0,
assert.containsNone(
target,
".o_statusbar_status > :not(.d-none)",
"statusbar widget should be empty"
);
});
@ -308,9 +306,8 @@ QUnit.module("Fields", (hooks) => {
});
assert.doesNotHaveClass(target.querySelector(".o_statusbar_status"), "o_field_empty");
const tooltipInfo = target.querySelector(".o_field_statusbar").attributes[
"data-tooltip-info"
];
const tooltipInfo =
target.querySelector(".o_field_statusbar").attributes["data-tooltip-info"];
assert.strictEqual(
JSON.parse(tooltipInfo.value).field.help,
"some info about the field",
@ -407,6 +404,7 @@ QUnit.module("Fields", (hooks) => {
".o_statusbar_status button:not(.dropdown-toggle)"
);
await click(buttons[buttons.length - 1]);
await nextTick();
assert.containsN(target, ".o_statusbar_status button:not(.dropdown-toggle)", 2);
}
);
@ -434,15 +432,15 @@ QUnit.module("Fields", (hooks) => {
</form>`,
});
await click(target, ".o_statusbar_status .dropdown-toggle");
await click(target, ".o_statusbar_status .dropdown-toggle:not(.d-none)");
const status = target.querySelectorAll(".o_statusbar_status");
assert.containsOnce(status[0], ".dropdown-item.disabled");
assert.containsOnce(status[status.length - 1], "button.disabled");
assert.containsOnce(status[status.length - 1], "button:disabled");
}
);
QUnit.test("statusbar: choose an item from the 'More' menu", async function (assert) {
QUnit.test("statusbar: choose an item from the folded menu", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
@ -471,11 +469,11 @@ QUnit.module("Fields", (hooks) => {
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_statusbar_status .dropdown-toggle:not(.d-none)");
await click(target, ".o-dropdown .dropdown-item");
assert.strictEqual(
target.querySelector("[aria-checked='true']").textContent,
@ -509,10 +507,10 @@ QUnit.module("Fields", (hooks) => {
},
});
assert.containsN(target, ".o_statusbar_status button.disabled", 3);
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.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");
@ -551,35 +549,7 @@ QUnit.module("Fields", (hooks) => {
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) {
QUnit.test("smart actions are unavailable if readonly", async function (assert) {
await makeView({
serverData,
type: "form",
@ -594,7 +564,34 @@ QUnit.module("Fields", (hooks) => {
});
assert.containsOnce(target, ".o_field_widget");
triggerHotkey("alt+shift+x");
triggerHotkey("control+k");
await nextTick();
const moveStages = [...target.querySelectorAll(".o_command")].map((el) => el.textContent);
assert.notOk(moveStages.includes("Move to Trululu...ALT + SHIFT + X"));
assert.notOk(moveStages.includes("Move to next...ALT + X"));
});
QUnit.test("hotkeys are 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"); // Move to stage...
await nextTick();
assert.containsNone(target, ".modal", "command palette should not open");
triggerHotkey("alt+x"); // Move to next
await nextTick();
assert.containsNone(target, ".modal", "command palette should not open");
});
@ -612,8 +609,8 @@ QUnit.module("Fields", (hooks) => {
</header>
</form>`,
mockRPC(_route, { method }) {
if (method === "write") {
assert.step("write");
if (method === "web_save") {
assert.step("web_save");
}
},
});
@ -621,9 +618,99 @@ QUnit.module("Fields", (hooks) => {
".o_statusbar_status button.btn:not(.dropdown-toggle):not(:disabled):not(.o_arrow_button_current)"
);
await click(clickableButtons[clickableButtons.length - 1]);
assert.verifySteps(["write"]);
assert.verifySteps(["web_save"]);
});
QUnit.test(
"For the same record, a single rpc is done to recover the specialData",
async function (assert) {
serverData.views = {
"partner,3,list": '<tree><field name="display_name"/></tree>',
"partner,9,search": `<search></search>`,
"partner,false,form": `<form>
<header>
<field name="trululu" widget="statusbar" readonly="1"/>
</header>
</form>`,
};
serverData.actions = {
1: {
id: 1,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
};
const mockRPC = (route, args) => {
if (args.method === "search_read") {
assert.step("search_read");
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.verifySteps(["search_read"]);
await click(target, ".o_back_button");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.verifySteps([]);
}
);
QUnit.test(
"open form with statusbar, leave and come back to another one with other domain",
async function (assert) {
serverData.views = {
"partner,3,list": '<tree><field name="display_name"/></tree>',
"partner,9,search": `<search></search>`,
"partner,false,form": `<form>
<header>
<field name="trululu" widget="statusbar" domain="[['id', '>', id]]" readonly="1"/>
</header>
</form>`,
};
serverData.actions = {
1: {
id: 1,
name: "Partners",
res_model: "partner",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
},
};
const mockRPC = (route, args) => {
if (args.method === "search_read") {
assert.step("search_read");
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
// open first record
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.verifySteps(["search_read"]);
// go back and open second record
await click(target, ".o_back_button");
await click(target.querySelectorAll(".o_data_row")[1].querySelector(".o_data_cell"));
assert.verifySteps(["search_read"]);
}
);
QUnit.test(
"clickable statusbar with readonly modifier set to false is editable",
async function (assert) {
@ -635,12 +722,15 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': false}"/>
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="False"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button:visible", 2);
assert.containsNone(target, ".o_statusbar_status button.disabled[disabled]:visible");
assert.containsNone(
target,
".o_statusbar_status button[disabled][aria-checked='false']:visible"
);
}
);
@ -655,11 +745,11 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': true}" attrs="{'readonly': true}"/>
<field name="product_id" widget="statusbar" options="{'clickable': true}" readonly="True"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
assert.containsN(target, ".o_statusbar_status button[disabled]:visible", 2);
}
);
@ -674,11 +764,176 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<header>
<field name="product_id" widget="statusbar" options="{'clickable': false}" attrs="{'readonly': false}"/>
<field name="product_id" widget="statusbar" options="{'clickable': false}" readonly="False"/>
</header>
</form>`,
});
assert.containsN(target, ".o_statusbar_status button.disabled[disabled]:visible", 2);
assert.containsN(target, ".o_statusbar_status button[disabled]:visible", 2);
}
);
QUnit.test(
"last status bar button have a border radius (no arrow shape) on the right side when a prior folded stage gets selected",
async function (assert) {
serverData.models = {
stage: {
fields: {
name: { string: "Name", type: "char" },
folded: { string: "Folded", type: "boolean", default: false },
},
records: [
{ id: 1, name: "New" },
{ id: 2, name: "In Progress", folded: true },
{ id: 3, name: "Done" },
],
},
task: {
fields: {
status: { string: "Status", type: "many2one", relation: "stage" },
},
records: [
{ id: 1, status: 1 },
{ id: 2, status: 2 },
{ id: 3, status: 3 },
],
},
};
await makeView({
type: "form",
resModel: "task",
resId: 3,
serverData,
arch: `
<form>
<header>
<field name="status" widget="statusbar" options="{'clickable': true, 'fold_field': 'folded'}" />
</header>
</form>`,
});
await click(target, ".o_statusbar_status .dropdown-toggle:not(.d-none)");
await click(target, ".o-dropdown .dropdown-item");
const button = target.querySelector(".o_statusbar_status button[data-value='3']");
assert.notEqual(button.style.borderTopRightRadius, "0px");
assert.hasClass(button, "o_first");
}
);
QUnit.test("correctly load statusbar when dynamic domain changes", async function (assert) {
serverData.models = {
stage: {
fields: {
name: { string: "Name", type: "char" },
folded: { string: "Folded", type: "boolean", default: false },
project_ids: { string: "Project", type: "many2many", relation: "project" },
},
records: [
{ id: 1, name: "Stage Project 1", project_ids: [1] },
{ id: 2, name: "Stage Project 2", project_ids: [2] },
],
},
project: {
fields: {
display_name: { string: "Name", type: "char" },
},
records: [
{ id: 1, display_name: "Project 1" },
{ id: 2, display_name: "Project 2" },
],
},
task: {
fields: {
status: { string: "Status", type: "many2one", relation: "stage" },
project_id: { string: "Project", type: "many2one", relation: "project" },
},
records: [{ id: 1, project_id: 1, status: 1 }],
},
};
serverData.models.task.onchanges = {
project_id: (obj) => {
obj.status = obj.project_id === 1 ? 1 : 2;
},
};
await makeView({
type: "form",
resModel: "task",
resId: 1,
serverData,
arch: `
<form>
<header>
<field name="status" widget="statusbar" domain="[('project_ids', 'in', project_id)]" />
</header>
<field name="project_id"/>
</form>`,
mockRPC(route, args) {
if (args.method === "search_read") {
assert.step(JSON.stringify(args.kwargs.domain));
}
},
});
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_statusbar_status button:not(.d-none)")),
["Stage Project 1"]
);
assert.verifySteps(['["|",["id","=",1],["project_ids","in",1]]']);
await selectDropdownItem(target, "project_id", "Project 2");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_statusbar_status button:not(.d-none)")),
["Stage Project 2"]
);
assert.verifySteps(['["|",["id","=",2],["project_ids","in",2]]']);
await clickSave(target);
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_statusbar_status button:not(.d-none)")),
["Stage Project 2"]
);
assert.verifySteps([]);
});
QUnit.test('"status" with no stages does not crash command palette', async function (assert) {
serverData.models = {
stage: {
fields: {
name: { string: "Stage Name", type: "char" },
},
records: [],
},
task: {
fields: {
status: { string: "Stage", type: "many2one", relation: "stage" },
},
records: [
{ id: 1, status: false }, // no stage set
],
},
};
await makeView({
serverData,
type: "form",
resModel: "task",
arch: `
<form>
<header>
<field name="status" widget="statusbar" options="{'withCommand': true, 'clickable': true}"/>
</header>
</form>`,
resId: 1,
});
// Open the command palette (Ctrl+K)
triggerHotkey("control+k");
await nextTick();
const commands = [...target.querySelectorAll(".o_command")].map((el) => el.textContent);
assert.notOk(
commands.some((txt) => txt.includes("Move to next Stage")),
"No 'Move to next stage' command available when no stages exist"
);
});
});

View file

@ -8,7 +8,9 @@ import {
clickSave,
editInput,
getFixture,
nextTick,
triggerEvent,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
@ -20,7 +22,6 @@ let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
@ -56,6 +57,17 @@ QUnit.module("Fields", (hooks) => {
},
],
},
partner_list: {
fields: {
partner_ids: {
string: "Partners",
type: "one2many",
relation: "partner",
relation_field: "id",
},
},
records: [{ id: 1, partner_ids: [1] }],
},
},
};
@ -159,7 +171,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="bar" />
<field name="txt" attrs="{'invisible': [('bar', '=', True)]}" />
<field name="txt" invisible="bar" />
</form>`,
});
@ -255,11 +267,7 @@ QUnit.module("Fields", (hooks) => {
});
const textarea = target.querySelector("textarea");
assert.strictEqual(
textarea.rows,
4,
"rowCount should be the one set on the field",
);
assert.strictEqual(textarea.rows, 4, "rowCount should be the one set on the field");
});
QUnit.test(
@ -282,8 +290,9 @@ QUnit.module("Fields", (hooks) => {
});
// ensure that autoresize is correctly done
let height = target.querySelector(".o_field_widget[name=text_field] textarea")
.offsetHeight;
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(
@ -352,8 +361,9 @@ QUnit.module("Fields", (hooks) => {
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;
height = target.querySelector(
".o_field_widget[name=text_field_empty] textarea"
).offsetHeight;
assert.strictEqual(height, 50, "empty textarea should have height of 50px");
});
@ -584,7 +594,11 @@ QUnit.module("Fields", (hooks) => {
arch: '<tree editable="top"><field name="foo"/></tree>',
});
await click(target.querySelector(".o_list_button_add"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_add"
)
);
assert.strictEqual(
target.querySelector("textarea"),
@ -592,4 +606,96 @@ QUnit.module("Fields", (hooks) => {
"text area should have the focus"
);
});
QUnit.test("field text with dynamic placeholder", async (assert) => {
serverData.models.partner.fields.model_reference_field = {
string: "Model Reference Field",
type: "char",
default: "partner",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="model_reference_field" invisible="1"/>
<sheet>
<group>
<field
name="txt"
options="{
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'model_reference_field'
}"
/>
</group>
</sheet>
</form>`,
});
await click(target, "[name=txt] textarea");
assert.strictEqual(document.activeElement, target.querySelector("[name=txt] textarea"));
assert.containsNone(document.body, ".o_popover .o_model_field_selector_popover");
triggerHotkey("#");
await nextTick();
assert.containsOnce(document.body, ".o_popover .o_model_field_selector_popover");
});
QUnit.test("text field should vertical autoresize when saving", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
serverData.models.partner.records[0].foo = "1";
await makeView({
type: "form",
resModel: "partner_list",
resId: 1,
serverData,
arch: `
<form>
<field name="partner_ids" widget="one2many">
<tree editable="bottom">
<field name="foo" widget="text"/>
</tree>
</field>
</form>`,
});
await click(target, "[name=foo] div");
let textarea = target.querySelector(".o_field_widget[name='foo'] textarea");
const initialHeight = textarea.offsetHeight;
await editInput(textarea, null, "1\n2\n3\n4\n5\n6\n7\n8");
await clickSave(target);
await click(target, "[name=foo] div");
textarea = target.querySelector(".o_field_widget[name='foo'] textarea");
const afterHeight = textarea.offsetHeight;
assert.ok(afterHeight > initialHeight, "Should be taller than one character");
});
QUnit.test("text field without line breaks", async function (assert) {
serverData.models.partner.fields.foo.type = "text";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form><field name="foo" options="{'line_breaks': False}"/></form>`,
});
assert.containsOnce(target, ".o_field_text textarea", "should have a text area");
const textarea = target.querySelector(".o_field_text textarea");
assert.strictEqual(textarea.value, "yop");
textarea.focus();
const keydownEvent = await triggerEvent(textarea, null, "keydown", { key: "Enter" });
assert.strictEqual(keydownEvent.defaultPrevented, true);
assert.strictEqual(textarea.value, "yop", "no line break should appear");
// Simulate a (very artificial) paste event
textarea.value = "text\nwith\nline\nbreaks\n";
await triggerEvent(textarea, null, "input", { inputType: "insertFromPaste" });
assert.strictEqual(textarea.value, "text with line breaks ", "no line break should appear");
});
});

View file

@ -55,7 +55,7 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
arch: /*xml*/ `
<tree string="Colors" editable="top">
<field name="tz_offset" invisible="True"/>
<field name="tz_offset" column_invisible="True"/>
<field name="color" widget="timezone_mismatch" />
</tree>
`,

View file

@ -226,7 +226,11 @@ QUnit.module("Fields", (hooks) => {
await editInput(cell, "input", "brolo");
// save
await click(target.querySelector(".o_list_button_save"));
await click(
target.querySelector(
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o_list_button_save"
)
);
cell = target.querySelector("tbody td:not(.o_list_record_selector)");
assert.doesNotHaveClass(
cell.parentElement,
@ -272,7 +276,7 @@ QUnit.module("Fields", (hooks) => {
<form>
<sheet>
<group>
<field name="foo" widget="url" attrs="{'readonly': True}" />
<field name="foo" widget="url" readonly="True" />
<field name="foo2" />
</group>
</sheet>