import { expect, test } from "@odoo/hoot"; import { click, edit, manuallyDispatchProgrammaticEvent, queryAll, queryFirst, setInputFiles, waitFor, } from "@odoo/hoot-dom"; import { animationFrame, runAllTimers } from "@odoo/hoot-mock"; import { clickSave, defineModels, fields, models, mountView, onRpc, pagerNext, } from "@web/../tests/web_test_helpers"; import { getOrigin } from "@web/core/utils/urls"; const MY_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; const PRODUCT_IMAGE = "R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7"; function getUnique(target) { const src = target.dataset.src; return new URL(src).searchParams.get("unique"); } async function setFiles(files) { await click("input[type=file]", { visible: false }); await setInputFiles(files); await waitFor(`div[name=document] img[data-src^="data:image/"]`, { timeout: 1000 }); } class Partner extends models.Model { name = fields.Char(); timmy = fields.Many2many({ relation: "partner.type" }); foo = fields.Char(); document = fields.Binary(); _records = [ { id: 1, name: "first record", timmy: [], document: "coucou==" }, { id: 2, name: "second record", timmy: [] }, { id: 4, name: "aaa" }, ]; } class PartnerType extends models.Model { _name = "partner.type"; name = fields.Char(); color = fields.Integer(); _records = [ { id: 12, name: "gold", color: 2 }, { id: 14, name: "silver", color: 5 }, ]; } defineModels([Partner, PartnerType]); test("ImageField is correctly rendered", async () => { expect.assertions(10); Partner._records[0].write_date = "2017-02-08 10:00:00"; Partner._records[0].document = MY_IMAGE; onRpc("web_read", ({ kwargs }) => { expect(kwargs.specification).toEqual( { display_name: {}, document: {}, write_date: {}, }, { message: "The fields document, name and write_date should be present when reading an image", } ); }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ `
`, }); expect(".o_field_widget[name='document']").toHaveClass("o_field_image", { message: "the widget should have the correct class", }); expect(".o_field_widget[name='document'] img").toHaveCount(1, { message: "the widget should contain an image", }); expect('div[name="document"] img').toHaveAttribute( "data-src", `data:image/png;base64,${MY_IMAGE}`, { message: "the image should have the correct src" } ); expect(".o_field_widget[name='document'] img").toHaveClass("img-fluid", { message: "the image should have the correct class", }); expect(".o_field_widget[name='document'] img").toHaveAttribute("width", "90", { message: "the image should correctly set its attributes", }); expect(".o_field_widget[name='document'] img").toHaveStyle( { maxWidth: "90px", width: "90px", height: "90px", }, { message: "the image should correctly set its attributes", } ); expect(".o_field_image .o_select_file_button").toHaveCount(1, { message: "the image can be edited", }); expect(".o_field_image .o_clear_file_button").toHaveCount(1, { message: "the image can be deleted", }); expect("input.o_input_file").toHaveAttribute("accept", "image/*", { message: 'the default value for the attribute "accept" on the "image" widget must be "image/*"', }); }); test("ImageField with img_class option", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: ` `, }); expect(".o_field_image img").toHaveClass("my_custom_class"); }); test("ImageField with alt attribute", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: ` `, }); expect(".o_field_widget[name='document'] img").toHaveAttribute("data-alt", "something", { message: "the image should correctly set its alt attribute", }); }); test("ImageField on a many2one", async () => { Partner._fields.parent_id = fields.Many2one({ relation: "partner" }); Partner._records[1].parent_id = 1; await mountView({ type: "form", resModel: "partner", resId: 2, arch: /* xml */ ` `, }); expect(".o_field_widget[name=parent_id] img").toHaveCount(1); expect('div[name="parent_id"] img').toHaveAttribute( "data-src", `${getOrigin()}/web/image/partner/1/document` ); expect(".o_field_widget[name='parent_id'] img").toHaveAttribute("data-alt", "first record"); }); test("ImageField is correctly replaced when given an incorrect value", async () => { Partner._records[0].document = "incorrect_base64_value"; await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); expect(`div[name="document"] img`).toHaveAttribute( "data-src", "data:image/png;base64,incorrect_base64_value", { message: "the image has the invalid src by default", } ); // As GET requests can't occur in tests, we must generate an error // on the img element to check whether the data-src is replaced with // a placeholder, here knowing that the GET request would fail manuallyDispatchProgrammaticEvent(queryFirst('div[name="document"] img'), "error"); await animationFrame(); expect('.o_field_widget[name="document"]').toHaveClass("o_field_image", { message: "the widget should have the correct class", }); expect(".o_field_widget[name='document'] img").toHaveCount(1, { message: "the widget should contain an image", }); expect('div[name="document"] img').toHaveAttribute( "data-src", "/web/static/img/placeholder.png", { message: "the image should have the correct src" } ); expect(".o_field_widget[name='document'] img").toHaveClass("img-fluid", { message: "the image should have the correct class", }); expect(".o_field_widget[name='document'] img").toHaveAttribute("width", "90", { message: "the image should correctly set its attributes", }); expect(".o_field_widget[name='document'] img").toHaveStyle("maxWidth: 90px", { message: "the image should correctly set its attributes", }); expect(".o_field_image .o_select_file_button").toHaveCount(1, { message: "the image can be edited", }); expect(".o_field_image .o_clear_file_button").toHaveCount(0, { message: "the image cannot be deleted as it has not been uploaded", }); }); test("ImageField preview is updated when an image is uploaded", async () => { const imageFile = new File( [Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))], "fake_file.png", { type: "png" } ); await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); expect('div[name="document"] img').toHaveAttribute( "data-src", "data:image/png;base64,coucou==", { message: "the image should have the initial src" } ); // Whitebox: replace the event target before the event is handled by the field so that we can modify // the files that it will take into account. This relies on the fact that it reads the files from // event.target and not from a direct reference to the input element. await click(".o_select_file_button"); await setInputFiles(imageFile); // It can take some time to encode the data as a base64 url await runAllTimers(); // Wait for a render await animationFrame(); expect("div[name=document] img").toHaveAttribute( "data-src", `data:image/png;base64,${MY_IMAGE}`, { message: "the image should have the new src" } ); }); test("clicking save manually after uploading new image should change the unique of the image src", async () => { Partner._onChanges.foo = () => {}; const rec = Partner._records.find((rec) => rec.id === 1); rec.document = "3 kb"; rec.write_date = "2022-08-05 08:37:00"; // 1659688620000 // 1659692220000, 1659695820000 const lastUpdates = ["2022-08-05 09:37:00", "2022-08-05 10:37:00"]; let index = 0; onRpc("web_save", ({ args }) => { args[1].write_date = lastUpdates[index]; args[1].document = "4 kb"; index++; }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000"); await click("input[type=file]", { visible: false }); await setFiles( new File( [Uint8Array.from([...atob(MY_IMAGE)].map((c) => c.charCodeAt(0)))], "fake_file.png", { type: "png" } ) ); expect("div[name=document] img").toHaveAttribute( "data-src", `data:image/png;base64,${MY_IMAGE}` ); await click(".o_field_widget[name='foo'] input"); await edit("grrr"); await animationFrame(); expect("div[name=document] img").toHaveAttribute( "data-src", `data:image/png;base64,${MY_IMAGE}` ); await clickSave(); expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000"); // Change the image again. After clicking save, it should have the correct new url. await click("input[type=file]", { visible: false }); await setFiles( new File( [Uint8Array.from([...atob(PRODUCT_IMAGE)].map((c) => c.charCodeAt(0)))], "fake_file2.gif", { type: "gif" } ) ); expect("div[name=document] img").toHaveAttribute( "data-src", `data:image/gif;base64,${PRODUCT_IMAGE}` ); await clickSave(); expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659695820000"); }); test("save record with image field modified by onchange", async () => { Partner._onChanges.foo = (data) => { data.document = MY_IMAGE; }; const rec = Partner._records.find((rec) => rec.id === 1); rec.document = "3 kb"; rec.write_date = "2022-08-05 08:37:00"; // 1659688620000 // 1659692220000 const lastUpdates = ["2022-08-05 09:37:00"]; let index = 0; onRpc("web_save", ({ args }) => { args[1].write_date = lastUpdates[index]; args[1].document = "3 kb"; index++; }); await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659688620000"); await click("[name='foo'] input"); await edit("grrr", { confirm: "enter" }); await animationFrame(); expect("div[name=document] img").toHaveAttribute( "data-src", `data:image/png;base64,${MY_IMAGE}` ); await clickSave(); expect(getUnique(queryFirst(".o_field_image img"))).toBe("1659692220000"); }); test("ImageField: option accepted_file_extensions", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); // The view must be in edit mode expect("input.o_input_file").toHaveAttribute("accept", ".png,.jpeg", { message: "the input should have the correct ``accept`` attribute", }); }); test("ImageField: set 0 width/height in the size option", async () => { await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); const imgs = queryAll(".o_field_widget img"); expect([imgs[0].attributes.width, imgs[0].attributes.height]).toEqual([undefined, undefined], { message: "if both size are set to 0, both attributes are undefined", }); expect([imgs[1].attributes.width, imgs[1].attributes.height.value]).toEqual([undefined, "50"], { message: "if only the width is set to 0, the width attribute is not set on the img", }); expect([ imgs[1].style.width, imgs[1].style.maxWidth, imgs[1].style.height, imgs[1].style.maxHeight, ]).toEqual(["auto", "100%", "", "50px"], { message: "the image should correctly set its attributes", }); expect([imgs[2].attributes.width.value, imgs[2].attributes.height]).toEqual(["50", undefined], { message: "if only the height is set to 0, the height attribute is not set on the img", }); expect([ imgs[2].style.width, imgs[2].style.maxWidth, imgs[2].style.height, imgs[2].style.maxHeight, ]).toEqual(["", "50px", "auto", "100%"], { message: "the image should correctly set its attributes", }); }); test("ImageField: zoom and zoom_delay options (readonly)", async () => { Partner._records[0].document = MY_IMAGE; await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); // data-tooltip attribute is used by the tooltip service expect(".o_field_image img").toHaveAttribute( "data-tooltip-info", `{"url":"data:image/png;base64,${MY_IMAGE}"}`, { message: "shows a tooltip on hover" } ); expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", { message: "tooltip has the right delay", }); }); test("ImageField: zoom and zoom_delay options (edit)", async () => { Partner._records[0].document = "3 kb"; Partner._records[0].write_date = "2022-08-05 08:37:00"; await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); expect(".o_field_image img").toHaveAttribute( "data-tooltip-info", `{"url":"${getOrigin()}/web/image/partner/1/document?unique=1659688620000"}`, { message: "tooltip show the full image from the field value" } ); expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", { message: "tooltip has the right delay", }); }); test("ImageField displays the right images with zoom and preview_image options (readonly)", async () => { Partner._records[0].document = "3 kb"; Partner._records[0].write_date = "2022-08-05 08:37:00"; await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); expect(".o_field_image img").toHaveAttribute( "data-tooltip-info", `{"url":"${getOrigin()}/web/image/partner/1/document?unique=1659688620000"}`, { message: "tooltip show the full image from the field value" } ); expect(".o_field_image img").toHaveAttribute("data-tooltip-delay", "600", { message: "tooltip has the right delay", }); }); test("ImageField in subviews is loaded correctly", async () => { Partner._records[0].write_date = "2017-02-08 10:00:00"; Partner._records[0].document = MY_IMAGE; PartnerType._fields.image = fields.Binary({}); PartnerType._records[0].image = PRODUCT_IMAGE; Partner._records[0].timmy = [12]; await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ ` `, }); expect(`img[data-src="data:image/png;base64,${MY_IMAGE}"]`).toHaveCount(1); expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1); // Actual flow: click on an element of the m2m to get its form view await click(".o_kanban_record:not(.o_kanban_ghost)"); await animationFrame(); expect(".modal").toHaveCount(1, { message: "The modal should have opened" }); expect(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`).toHaveCount(1); }); test("ImageField in x2many list is loaded correctly", async () => { PartnerType._fields.image = fields.Binary({}); PartnerType._records[0].image = PRODUCT_IMAGE; Partner._records[0].timmy = [12]; await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ `