/** @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: `
`, 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: `
`, }); 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: `
`, }); 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 */ `
`, 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 */ `
`, 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: `
`, }); // 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: `
`, }); 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: `
`, }); // 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: `
`, }); 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: `
`, }); 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: `
`, }); 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: `
`, }); 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: `
`, 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: `
`, }); 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: `
`, 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: `
`, }); 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: `
`, }); 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: `
`, 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 ); } ); });