Odoo18-Base/addons/web/static/tests/views/fields/image_field_tests.js
2025-03-10 11:12:23 +07:00

961 lines
34 KiB
JavaScript

/** @odoo-module **/
import {
click,
getFixture,
nextTick,
triggerEvent,
clickSave,
editInput,
patchDate,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { pagerNext } from "@web/../tests/search/helpers";
const MY_IMAGE =
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
const PRODUCT_IMAGE =
"R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7";
let serverData;
let target;
function getUnique(target) {
const src = target.dataset.src;
return new URL(src).searchParams.get("unique");
}
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
display_name: {
string: "Displayed name",
type: "char",
searchable: true,
},
timmy: {
string: "pokemon",
type: "many2many",
relation: "partner_type",
searchable: true,
},
foo: { type: "char" },
document: { string: "Binary", type: "binary" },
},
records: [
{
id: 1,
display_name: "first record",
timmy: [],
document: "coucou==",
},
{
id: 2,
display_name: "second record",
timmy: [],
},
{
id: 4,
display_name: "aaa",
},
],
},
partner_type: {
fields: {
name: { string: "Partner Type", type: "char", searchable: true },
color: { string: "Color index", type: "integer", searchable: true },
},
records: [
{ id: 12, display_name: "gold", color: 2 },
{ id: 14, display_name: "silver", color: 5 },
],
},
},
};
setupViewRegistries();
});
QUnit.module("ImageField");
QUnit.test("ImageField is correctly rendered", async function (assert) {
assert.expect(12);
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
serverData.models.partner.records[0].document = MY_IMAGE;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'size': [90, 90]}" />
</form>`,
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/partner/read") {
assert.deepEqual(
args[1],
["__last_update", "document", "display_name"],
"The fields document, display_name and __last_update should be present when reading an image"
);
}
},
});
assert.hasClass(
target.querySelector(".o_field_widget[name='document']"),
"o_field_image",
"the widget should have the correct class"
);
assert.containsOnce(
target,
".o_field_widget[name='document'] img",
"the widget should contain an image"
);
assert.strictEqual(
target.querySelector('div[name="document"] img').dataset.src,
`data:image/png;base64,${MY_IMAGE}`,
"the image should have the correct src"
);
assert.hasClass(
target.querySelector(".o_field_widget[name='document'] img"),
"img-fluid",
"the image should have the correct class"
);
assert.hasAttrValue(
target.querySelector(".o_field_widget[name='document'] img"),
"width",
"90",
"the image should correctly set its attributes"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name='document'] img").style.maxWidth,
"90px",
"the image should correctly set its attributes"
);
const computedStyle = window.getComputedStyle(
target.querySelector(".o_field_widget[name='document'] img")
);
assert.strictEqual(
computedStyle.width,
"90px",
"the image should correctly set its attributes"
);
assert.strictEqual(
computedStyle.height,
"90px",
"the image should correctly set its attributes"
);
assert.containsOnce(
target,
".o_field_image .o_select_file_button",
"the image can be edited"
);
assert.containsOnce(
target,
".o_field_image .o_clear_file_button",
"the image can be deleted"
);
assert.strictEqual(
target.querySelector("input.o_input_file").getAttribute("accept"),
"image/*",
'the default value for the attribute "accept" on the "image" widget must be "image/*"'
);
});
QUnit.test(
"ImageField is correctly replaced when given an incorrect value",
async function (assert) {
serverData.models.partner.records[0].document = "incorrect_base64_value";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'size': [90, 90]}"/>
</form>`,
});
assert.strictEqual(
target.querySelector('div[name="document"] img').dataset.src,
"data:image/png;base64,incorrect_base64_value",
"the image has the invalid src by default"
);
// As GET requests can't occur in tests, we must generate an error
// on the img element to check whether the data-src is replaced with
// a placeholder, here knowing that the GET request would fail
await triggerEvent(target, 'div[name="document"] img', "error");
assert.hasClass(
target.querySelector('.o_field_widget[name="document"]'),
"o_field_image",
"the widget should have the correct class"
);
assert.containsOnce(
target,
".o_field_widget[name='document'] img",
"the widget should contain an image"
);
assert.strictEqual(
target.querySelector('div[name="document"] img').dataset.src,
"/web/static/img/placeholder.png",
"the image should have the correct src"
);
assert.hasClass(
target.querySelector(".o_field_widget[name='document'] img"),
"img-fluid",
"the image should have the correct class"
);
assert.hasAttrValue(
target.querySelector(".o_field_widget[name='document'] img"),
"width",
"90",
"the image should correctly set its attributes"
);
assert.strictEqual(
target.querySelector(".o_field_widget[name='document'] img").style.maxWidth,
"90px",
"the image should correctly set its attributes"
);
assert.containsOnce(
target,
".o_field_image .o_select_file_button",
"the image can be edited"
);
assert.containsNone(
target,
".o_field_image .o_clear_file_button",
"the image cannot be deleted as it has not been uploaded"
);
}
);
QUnit.test("ImageField preview is updated when an image is uploaded", async function (assert) {
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `<form>
<field name="document" widget="image" options="{'size': [90, 90]}"/>
</form>`,
});
assert.strictEqual(
target.querySelector('div[name="document"] img').dataset.src,
"data:image/png;base64,coucou==",
"the image should have the initial src"
);
// Whitebox: replace the event target before the event is handled by the field so that we can modify
// the files that it will take into account. This relies on the fact that it reads the files from
// event.target and not from a direct reference to the input element.
const fileInput = target.querySelector("input[type=file]");
const fakeInput = {
files: [new File([imageData], "fake_file.png", { type: "png" })],
};
fileInput.addEventListener(
"change",
(ev) => {
Object.defineProperty(ev, "target", { value: fakeInput });
},
{ capture: true }
);
fileInput.dispatchEvent(new Event("change"));
// It can take some time to encode the data as a base64 url
await new Promise((resolve) => setTimeout(resolve, 50));
// Wait for a render
await nextTick();
assert.strictEqual(
target.querySelector("div[name=document] img").dataset.src,
`data:image/png;base64,${MY_IMAGE}`,
"the image should have the new src"
);
});
QUnit.test(
"clicking save manually after uploading new image should change the unique of the image src",
async function (assert) {
serverData.models.partner.onchanges = { foo: () => {} };
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.document = "3 kb";
rec.__last_update = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000, 1659695820000
const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"];
let index = 0;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: /* xml */ `
<form>
<field name="foo"/>
<field name="document" widget="image" />
</form>`,
mockRPC(_route, { method, args }) {
if (method === "write") {
args[1].__last_update = lastUpdates[index];
args[1].document = "4 kb";
index++;
}
},
});
assert.strictEqual(
getUnique(target.querySelector(".o_field_image img")),
"1659688620000"
);
await editInput(
target,
"input[type=file]",
new File(
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
"fake_file.png",
{ type: "png" }
)
);
assert.strictEqual(
target.querySelector("div[name=document] img").dataset.src,
`data:image/png;base64,${MY_IMAGE}`
);
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
assert.strictEqual(
target.querySelector("div[name=document] img").dataset.src,
`data:image/png;base64,${MY_IMAGE}`
);
await clickSave(target);
assert.strictEqual(
getUnique(target.querySelector(".o_field_image img")),
"1659692220000"
);
// Change the image again. After clicking save, it should have the correct new url.
await editInput(
target,
"input[type=file]",
new File(
[Uint8Array.from([...atob(PRODUCT_IMAGE)].map((c) => c.charCodeAt(0)))],
"fake_file2.gif",
{ type: "png" }
)
);
assert.strictEqual(
target.querySelector("div[name=document] img").dataset.src,
`data:image/gif;base64,${PRODUCT_IMAGE}`
);
await clickSave(target);
assert.strictEqual(
getUnique(target.querySelector(".o_field_image img")),
"1659695820000"
);
}
);
QUnit.test("save record with image field modified by onchange", async function (assert) {
serverData.models.partner.onchanges = {
foo: (data) => {
data.document = MY_IMAGE;
},
};
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.document = "3 kb";
rec.__last_update = "2022-08-05 08:37:00"; // 1659688620000
// 1659692220000
const lastUpdates = ["2022-08-05 09:37:00"];
let index = 0;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: /* xml */ `
<form>
<field name="foo"/>
<field name="document" widget="image" />
</form>`,
mockRPC(_route, { method, args }) {
if (method === "write") {
args[1].__last_update = lastUpdates[index];
args[1].document = "3 kb";
index++;
}
},
});
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
await editInput(target, "[name='foo'] input", "grrr");
assert.strictEqual(
target.querySelector("div[name=document] img").dataset.src,
`data:image/png;base64,${MY_IMAGE}`
);
await clickSave(target);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
});
QUnit.test("ImageField: option accepted_file_extensions", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'accepted_file_extensions': '.png,.jpeg'}" />
</form>`,
});
// The view must be in edit mode
assert.strictEqual(
target.querySelector("input.o_input_file").getAttribute("accept"),
".png,.jpeg",
"the input should have the correct ``accept`` attribute"
);
});
QUnit.test("ImageField: set 0 width/height in the size option", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'size': [0, 0]}" />
<field name="document" widget="image" options="{'size': [0, 50]}" />
<field name="document" widget="image" options="{'size': [50, 0]}" />
</form>`,
});
const imgs = target.querySelectorAll(".o_field_widget img");
assert.deepEqual(
[imgs[0].attributes.width, imgs[0].attributes.height],
[undefined, undefined],
"if both size are set to 0, both attributes are undefined"
);
assert.deepEqual(
[imgs[1].attributes.width, imgs[1].attributes.height.value],
[undefined, "50"],
"if only the width is set to 0, the width attribute is not set on the img"
);
assert.deepEqual(
[
imgs[1].style.width,
imgs[1].style.maxWidth,
imgs[1].style.height,
imgs[1].style.maxHeight,
],
["auto", "100%", "", "50px"],
"the image should correctly set its attributes"
);
assert.deepEqual(
[imgs[2].attributes.width.value, imgs[2].attributes.height],
["50", undefined],
"if only the height is set to 0, the height attribute is not set on the img"
);
assert.deepEqual(
[
imgs[2].style.width,
imgs[2].style.maxWidth,
imgs[2].style.height,
imgs[2].style.maxHeight,
],
["", "50px", "auto", "100%"],
"the image should correctly set its attributes"
);
});
QUnit.test("ImageField: zoom and zoom_delay options (readonly)", async (assert) => {
serverData.models.partner.records[0].document = MY_IMAGE;
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" readonly="1" />
</form>`,
});
// data-tooltip attribute is used by the tooltip service
assert.strictEqual(
JSON.parse(target.querySelector(".o_field_image img").dataset["tooltipInfo"]).url,
`data:image/png;base64,${MY_IMAGE}`,
"shows a tooltip on hover"
);
assert.strictEqual(
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
"600",
"tooltip has the right delay"
);
});
QUnit.test("ImageField: zoom and zoom_delay options (edit)", async function (assert) {
serverData.models.partner.records[0].document = "3 kb";
serverData.models.partner.records[0].__last_update = "2022-08-05 08:37:00";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'zoom': true, 'zoom_delay': 600}" />
</form>`,
});
assert.ok(
JSON.parse(
target.querySelector(".o_field_image img").dataset["tooltipInfo"]
).url.endsWith("/web/image?model=partner&id=1&field=document&unique=1659688620000"),
"tooltip show the full image from the field value"
);
assert.strictEqual(
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
"600",
"tooltip has the right delay"
);
});
QUnit.test(
"ImageField displays the right images with zoom and preview_image options (readonly)",
async function (assert) {
serverData.models.partner.records[0].document = "3 kb";
serverData.models.partner.records[0].__last_update = "2022-08-05 08:37:00";
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'zoom': true, 'preview_image': 'document_preview', 'zoom_delay': 600}" readonly="1" />
</form>`,
});
assert.ok(
JSON.parse(
target.querySelector(".o_field_image img").dataset["tooltipInfo"]
).url.endsWith("/web/image?model=partner&id=1&field=document&unique=1659688620000"),
"tooltip show the full image from the field value"
);
assert.strictEqual(
target.querySelector(".o_field_image img").dataset["tooltipDelay"],
"600",
"tooltip has the right delay"
);
assert.ok(
target
.querySelector(".o_field_image img")
.dataset.src.endsWith(
"/web/image?model=partner&id=1&field=document_preview&unique=1659688620000"
),
"image src is the preview image given in option"
);
}
);
QUnit.test("ImageField in subviews is loaded correctly", async function (assert) {
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
serverData.models.partner.records[0].document = MY_IMAGE;
serverData.models.partner_type.fields.image = {
name: "image",
type: "binary",
};
serverData.models.partner_type.records[0].image = PRODUCT_IMAGE;
serverData.models.partner.records[0].timmy = [12];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="document" widget="image" options="{'size': [90, 90]}" />
<field name="timmy" widget="many2many" mode="kanban">
<kanban>
<field name="display_name" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<span>
<t t-esc="record.display_name.value" />
</span>
</div>
</t>
</templates>
</kanban>
<form>
<field name="image" widget="image" />
</form>
</field>
</form>`,
});
assert.containsOnce(target, `img[data-src="data:image/png;base64,${MY_IMAGE}"]`);
assert.containsOnce(target, ".o_kanban_record .oe_kanban_global_click");
// Actual flow: click on an element of the m2m to get its form view
await click(target, ".oe_kanban_global_click");
assert.containsOnce(target, ".modal", "The modal should have opened");
assert.containsOnce(target, `img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`);
});
QUnit.test("ImageField in x2many list is loaded correctly", async function (assert) {
serverData.models.partner_type.fields.image = {
name: "image",
type: "binary",
};
serverData.models.partner_type.records[0].image = PRODUCT_IMAGE;
serverData.models.partner.records[0].timmy = [12];
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="timmy" widget="many2many">
<tree>
<field name="image" widget="image" />
</tree>
</field>
</form>`,
});
assert.containsOnce(target, "tr.o_data_row", "There should be one record in the many2many");
assert.ok(
document.querySelector(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`),
"The list's image is in the DOM"
);
});
QUnit.test("ImageField with required attribute", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="document" widget="image" required="1" />
</form>`,
mockRPC(route, { method }) {
if (method === "create") {
throw new Error("Should not do a create RPC with unset required image field");
}
},
});
await clickSave(target);
assert.containsOnce(
target.querySelector(".o_form_view"),
".o_form_editable",
"form view should still be editable"
);
assert.hasClass(
target.querySelector(".o_field_widget"),
"o_field_invalid",
"image field should be displayed as invalid"
);
});
QUnit.test("ImageField is reset when changing record", async function (assert) {
const imageData = Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)));
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `<form>
<field name="document" widget="image" options="{'size': [90, 90]}"/>
</form>`,
});
async function setFiles() {
const list = new DataTransfer();
list.items.add(new File([imageData], "fake_file.png", { type: "png" }));
const fileInput = target.querySelector("input[type=file]");
fileInput.files = list.files;
fileInput.dispatchEvent(new Event("change"));
// It can take some time to encode the data as a base64 url
await new Promise((resolve) => setTimeout(resolve, 50));
// Wait for a render
await nextTick();
}
assert.strictEqual(
target.querySelector("img[data-alt='Binary file']").dataset.src,
"/web/static/img/placeholder.png",
"image field should not be set"
);
await setFiles();
assert.ok(
target
.querySelector("img[data-alt='Binary file']")
.dataset.src.includes("data:image/png;base64"),
"image field should be set"
);
await clickSave(target);
await click(target, ".o_form_button_create");
assert.strictEqual(
target.querySelector("img[data-alt='Binary file']").dataset.src,
"/web/static/img/placeholder.png",
"image field should be reset"
);
await setFiles();
assert.ok(
target
.querySelector("img[data-alt='Binary file']")
.dataset.src.includes("data:image/png;base64"),
"image field should be set"
);
});
QUnit.test("unique in url doesn't change on onchange", async (assert) => {
serverData.models.partner.onchanges = {
foo: () => {},
};
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.document = "3 kb";
rec.__last_update = "2022-08-05 08:37:00";
await makeView({
resId: 1,
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="foo" />
<field name="document" widget="image" required="1" />
</form>`,
mockRPC(route, { method, args }) {
assert.step(method);
if (method === "write") {
// 1659692220000
args[1].__last_update = "2022-08-05 09:37:00";
}
},
});
assert.verifySteps(["get_views", "read"]);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
assert.verifySteps([]);
// same unique as before
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
assert.verifySteps(["onchange"]);
// also same unique
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
await clickSave(target);
assert.verifySteps(["write", "read"]);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
});
QUnit.test("unique in url change on record change", async (assert) => {
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.document = "3 kb";
rec.__last_update = "2022-08-05 08:37:00";
const rec2 = serverData.models.partner.records.find((rec) => rec.id === 2);
rec2.document = "3 kb";
rec2.__last_update = "2022-08-05 09:37:00";
await makeView({
resIds: [1, 2],
resId: 1,
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="document" widget="image" required="1" />
</form>`,
});
function getUnique(target) {
const src = target.dataset.src;
return new URL(src).searchParams.get("unique");
}
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659688620000");
await pagerNext(target);
assert.strictEqual(getUnique(target.querySelector(".o_field_image img")), "1659692220000");
});
QUnit.test(
"unique in url does not change on record change if no_reload option is set",
async (assert) => {
const rec = serverData.models.partner.records.find((rec) => rec.id === 1);
rec.document = "3 kb";
rec.__last_update = "2022-08-05 08:37:00";
await makeView({
resIds: [1, 2],
resId: 1,
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="document" widget="image" required="1" options="{'no_reload': true}" />
<field name="__last_update" />
</form>`,
});
function getUnique(target) {
const src = target.dataset.src;
return new URL(src).searchParams.get("unique");
}
assert.strictEqual(
getUnique(target.querySelector(".o_field_image img")),
"1659688620000"
);
await editInput(
target.querySelector(
"div[name='__last_update'] > div > input",
"2022-08-05 08:39:00"
)
);
await click(target, ".o_form_button_save");
assert.strictEqual(
getUnique(target.querySelector(".o_field_image img")),
"1659688620000"
);
}
);
QUnit.test(
"url should not use the record last updated date when the field is related",
async function (assert) {
serverData.models.partner.fields.related = {
name: "Binary",
type: "binary",
related: "user.image",
};
serverData.models.partner.fields.user = {
name: "User",
type: "many2one",
relation: "user",
default: 1,
};
serverData.models.user = {
fields: {
image: {
name: "Image",
type: "binary",
},
},
records: [
{
id: 1,
image: "3 kb",
},
],
};
serverData.models.partner.records[0].__last_update = "2017-02-08 10:00:00";
patchDate(2017, 1, 6, 11, 0, 0);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="foo" />
<field name="user"/>
<field name="related" widget="image"/>
</group>
</sheet>
</form>`,
async mockRPC(route, { args }, performRpc) {
if (route === "/web/dataset/call_kw/partner/read") {
const res = await performRpc(...arguments);
// The mockRPC doesn't implement related fields
res[0].related = "3 kb";
return res;
}
},
});
const initialUnique = Number(getUnique(target.querySelector(".o_field_image img")));
assert.ok(initialUnique - 1486375200000 < 100);
await editInput(target, ".o_field_widget[name='foo'] input", "grrr");
// the unique should be the same
assert.strictEqual(
initialUnique,
Number(getUnique(target.querySelector(".o_field_image img")))
);
patchDate(2017, 1, 9, 11, 0, 0);
await editInput(
target,
"input[type=file]",
new File(
[Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))],
"fake_file.png",
{ type: "png" }
)
);
assert.strictEqual(
target.querySelector(".o_field_image img").dataset.src,
`data:image/png;base64,${MY_IMAGE}`
);
patchDate(2017, 1, 9, 12, 0, 0);
await clickSave(target);
assert.ok(
Number(getUnique(target.querySelector(".o_field_image img"))) - 1486638000000 < 100
);
}
);
});