import { HtmlField } from "@html_editor/fields/html_field"; import { MediaDialog } from "@html_editor/main/media/media_dialog/media_dialog"; import { stripHistoryIds } from "@html_editor/others/collaboration/collaboration_odoo_plugin"; import { getEditableDescendants, getEmbeddedProps, } from "@html_editor/others/embedded_component_utils"; import { READONLY_MAIN_EMBEDDINGS } from "@html_editor/others/embedded_components/embedding_sets"; import { normalizeHTML, parseHTML } from "@html_editor/utils/html"; import { Wysiwyg } from "@html_editor/wysiwyg"; import { beforeEach, describe, expect, test } from "@odoo/hoot"; import { click, press, queryAll, queryAllTexts, queryOne, waitFor, waitUntil, } from "@odoo/hoot-dom"; import { Deferred, animationFrame, mockSendBeacon, tick } from "@odoo/hoot-mock"; import { onWillDestroy, xml } from "@odoo/owl"; import { clickSave, contains, defineModels, defineParams, fields, models, mountView, mountViewInDialog, onRpc, patchWithCleanup, serverState, } from "@web/../tests/web_test_helpers"; import { assets } from "@web/core/assets"; import { browser } from "@web/core/browser/browser"; import { patch } from "@web/core/utils/patch"; import { FormController } from "@web/views/form/form_controller"; import { Counter, EmbeddedWrapperMixin } from "./_helpers/embedded_component"; import { moveSelectionOutsideEditor, setSelection } from "./_helpers/selection"; import { insertText, pasteOdooEditorHtml, pasteText, undo } from "./_helpers/user_actions"; class Partner extends models.Model { txt = fields.Html({ trim: true }); name = fields.Char(); _records = [ { id: 1, name: "first", txt: "

first

" }, { id: 2, name: "second", txt: "

second

" }, ]; _onChanges = { name(record) { if (record.name) { record.txt = `

${record.name}

`; } }, }; } class IrAttachment extends models.Model { _name = "ir.attachment"; name = fields.Char(); description = fields.Char(); mimetype = fields.Char(); checksum = fields.Char(); url = fields.Char(); type = fields.Char(); res_id = fields.Integer(); res_model = fields.Char(); public = fields.Boolean(); access_token = fields.Char(); image_src = fields.Char(); image_width = fields.Integer(); image_height = fields.Integer(); original_id = fields.Many2one({ relation: "ir.attachment" }); _records = [ { id: 1, name: "image", description: "", mimetype: "image/png", checksum: false, url: "/web/image/123/transparent.png", type: "url", res_id: 0, res_model: false, public: true, access_token: false, image_src: "/web/image/123/transparent.png", image_width: 256, image_height: 256, }, ]; } defineModels([Partner, IrAttachment]); let htmlEditor; beforeEach(() => { patchWithCleanup(HtmlField.prototype, { onEditorLoad(editor) { htmlEditor = editor; return super.onEditorLoad(...arguments); }, }); }); function setSelectionInHtmlField(selector = "p", fieldName = "txt") { const anchorNode = queryOne(`[name='${fieldName}'] .odoo-editor-editable ${selector}`); setSelection({ anchorNode, anchorOffset: 0 }); return anchorNode; } test("html field in readonly", async () => { await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable").toHaveCount(0); expect(`[name="txt"] .o_readonly`).toHaveCount(1); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML("

first

"); await contains(`.o_pager_next`).click(); expect(".odoo-editor-editable").toHaveCount(0); expect(`[name="txt"] .o_readonly`).toHaveCount(1); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML("

second

"); await contains(`.o_pager_previous`).click(); expect(".odoo-editor-editable").toHaveCount(0); expect(`[name="txt"] .o_readonly`).toHaveCount(1); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML("

first

"); }); test("html field in readonly updated by onchange", async () => { await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable").toHaveCount(0); expect(`[name="txt"] .o_readonly`).toHaveCount(1); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML("

first

"); await contains(`.o_field_widget[name=name] input`).edit("hello"); expect(".odoo-editor-editable").toHaveCount(0); expect(`[name="txt"] .o_readonly`).toHaveCount(1); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML("

hello

"); }); test("html field in readonly with embedded components", async () => { patchWithCleanup(Counter, { template: xml` :`, }); const unpatch = patch(Counter.prototype, { setup() { super.setup(); onWillDestroy(() => { this.testOnWillDestroy?.(); }); }, testOnWillDestroy() { expect.step("destroyed"); }, }); // patchWithCleanup Array => cleanup keeps the last array entry set to undefined, // so it can not be used READONLY_MAIN_EMBEDDINGS.push({ name: "counter", Component: Counter, getProps: (host) => ({ ...getEmbeddedProps(host), }), }); Partner._records = [ { id: 1, txt: `
`, }, ]; Partner._onChanges = { name(record) { record.txt = `
`; }, }; await mountView({ type: "form", resId: 1, resIds: [1], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable").toHaveCount(0); expect(`[name="txt"] .o_readonly`).toHaveCount(1); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML( `
name:0
` ); click(".counter"); await animationFrame(); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML( `
name:1
` ); // trigger the onchange method for name, which will replace the txt value. await contains(`.o_field_widget[name=name] input`).edit("hello"); await animationFrame(); expect.verifySteps(["destroyed"]); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML( `
:0
` ); unpatch(); READONLY_MAIN_EMBEDDINGS.pop(); }); test("html field in readonly with embedded components and editable descendants", async () => { const Wrapper = EmbeddedWrapperMixin("editable"); // patchWithCleanup Array => cleanup keeps the last array entry set to undefined, // so it can not be used READONLY_MAIN_EMBEDDINGS.push( { name: "wrapper", Component: Wrapper, getProps: (host) => ({ host }), getEditableDescendants, }, { name: "counter", Component: Counter, } ); Partner._records = [ { id: 1, txt: `
`, }, ]; await mountView({ type: "form", resId: 1, resIds: [1], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable").toHaveCount(0); expect(`[name="txt"] .o_readonly`).toHaveCount(1); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML( `
Counter:0
` ); click(".counter"); await animationFrame(); expect(`[name="txt"] .o_readonly`).toHaveInnerHTML( `
Counter:1
` ); READONLY_MAIN_EMBEDDINGS.pop(); READONLY_MAIN_EMBEDDINGS.pop(); }); test("links should open on a new tab in readonly", async () => { Partner._records = [ { id: 1, txt: `

first

Relative link Internal link External link `, }, { id: 2, txt: `

second

Relative link Internal link External link `, }, ]; await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect("[name='txt'] p").toHaveText("first"); for (const link of queryAll("a")) { expect(link.getAttribute("target")).toBe("_blank"); expect(link.getAttribute("rel")).toBe("noreferrer"); } await contains(`.o_pager_next`).click(); expect("[name='txt'] p").toHaveText("second"); for (const link of queryAll("a")) { expect(link.getAttribute("target")).toBe("_blank"); expect(link.getAttribute("rel")).toBe("noreferrer"); } }); test("edit and save a html field", async () => { onRpc("web_save", ({ args }) => { expect(args[1]).toEqual({ txt: "

testfirst

", }); expect.step("web_save"); }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable p").toHaveText("first"); expect(`.o_form_button_save`).not.toBeVisible(); setSelectionInHtmlField(); await insertText(htmlEditor, "test"); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("testfirst"); expect(".o_form_button_save").toBeVisible(); await contains(".o_form_button_save").click(); expect.verifySteps(["web_save"]); expect(".odoo-editor-editable p").toHaveText("testfirst"); expect(`.o_form_button_save`).not.toBeVisible(); }); test("edit and save a html field containing JSON as some attribute values should keep the same wysiwyg", async () => { patchWithCleanup(Wysiwyg.prototype, { setup() { super.setup(); expect.step("Setup Wysiwyg"); }, }); onRpc("partner", "web_save", ({ args }) => { expect.step("web_save"); // server representation does not have HTML entities args[1].txt = `

content

first

`; }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); const value = JSON.stringify({ myString: "myString", }); pasteOdooEditorHtml(htmlEditor, `

content

`); const txtField = queryOne('.o_field_html[name="txt"] .odoo-editor-editable'); expect(txtField).toHaveInnerHTML( `

content

first

` ); expect.verifySteps(["Setup Wysiwyg"]); await clickSave(); expect.verifySteps(["web_save"]); }); test("edit a html field in new form view dialog and close the dialog with 'escape'", async () => { await mountViewInDialog({ type: "form", resModel: "partner", arch: `
`, }); expect(".modal").toHaveCount(1); expect(".odoo-editor-editable p").toHaveText(""); await contains("[name='txt'] .odoo-editor-editable").focus(); setSelectionInHtmlField(); await insertText(htmlEditor, "test"); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("test"); expect(".o_form_button_save").toBeVisible(); await press("escape"); await animationFrame(); expect(".modal").toHaveCount(0); }); test("onchange update html field in edition", async () => { onRpc("web_save", ({ args }) => { expect(args[1]).toEqual({ txt: "

testfirst

", }); expect.step("web_save"); }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable p").toHaveText("first"); await contains(`.o_field_widget[name=name] input`).edit("hello"); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("hello"); }); test("create new record and load it correctly", async () => { class Composer extends models.Model { linked_composer_id = fields.Many2one({ relation: "composer" }); name = fields.Char(); body = fields.Html({ trim: true }); _records = [ { id: 1, linked_composer_id: 2, name: "first", body: "

2

", }, { id: 2, name: "second", linked_composer_id: 1, body: "

", }, ]; // Necessary for mobile _views = { "kanban,false": ` `, }; _onChanges = { linked_composer_id(record) { record.body = `

${record.linked_composer_id}

`; }, }; } defineModels([Composer]); await mountView({ type: "form", resId: 1, resModel: "composer", arch: `
`, }); expect(".odoo-editor-editable").toHaveCount(1); expect(".odoo-editor-editable").toHaveInnerHTML("

2

"); await contains(".o_input#linked_composer_id_0").click(); await animationFrame(); await contains(".ui-menu-item:contains(first), .o_kanban_record:contains(first)").click(); await animationFrame(); expect(".odoo-editor-editable").toHaveInnerHTML("

1

"); await contains(".o_input#linked_composer_id_0").click(); await animationFrame(); await contains(".ui-menu-item:contains(second), .o_kanban_record:contains(second)").click(); await animationFrame(); expect(".odoo-editor-editable").toHaveInnerHTML("

2

"); }); test.tags("focus required"); test("edit html field and blur multiple time should apply 1 onchange", async () => { const def = new Deferred(); Partner._onChanges = { txt() {}, }; onRpc("partner", "onchange", async ({ args }) => { expect.step(`onchange: ${args[1].txt}`); await def; }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "Hello "); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Hello first

"); await contains("[name='name'] input").click(); expect.verifySteps(["onchange:

Hello first

"]); await contains("[name='txt'] .odoo-editor-editable").focus(); await contains("[name='name'] input").click(); def.resolve(); await animationFrame(); expect.verifySteps([]); }); test.tags("focus required"); test("edit an html field during an onchange", async () => { const def = new Deferred(); Partner._onChanges = { txt(record) { record.txt = "

New Value

"; }, }; onRpc("partner", "onchange", async ({ args }) => { expect.step(`onchange: ${args[1].txt}`); await def; }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "Hello "); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Hello first

"); await contains(".o_form_view").click(); expect.verifySteps(["onchange:

Hello first

"]); setSelectionInHtmlField(); await insertText(htmlEditor, "Yop "); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Yop Hello first

"); def.resolve(); await animationFrame(); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Yop Hello first

"); }); test("click on next/previous page", async () => { await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable p:contains(first)").toHaveCount(1); await contains(`.o_pager_next`).click(); expect(".odoo-editor-editable p:contains(second)").toHaveCount(1); await contains(`.o_pager_previous`).click(); expect(".odoo-editor-editable p:contains(first)").toHaveCount(1); }); test("edit and switch page", async () => { onRpc("web_save", ({ args }) => { expect(args[1]).toEqual({ txt: "

testfirst

", }); expect.step("web_save"); }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable p").toHaveText("first"); expect(`.o_form_button_save`).not.toBeVisible(); setSelectionInHtmlField(); await insertText(htmlEditor, "test"); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("testfirst"); expect(`.o_form_button_save`).toBeVisible(); await contains(`.o_pager_next`).click(); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("second"); expect(`.o_form_button_save`).not.toBeVisible(); expect.verifySteps(["web_save"]); await contains(`.o_pager_previous`).click(); expect(".odoo-editor-editable p").toHaveText("testfirst"); expect(`.o_form_button_save`).not.toBeVisible(); }); test("discard changes in html field in form", async () => { await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable p").toHaveText("first"); expect(`.o_form_button_save`).not.toBeVisible(); // move the hoot focus in the editor await click(".odoo-editor-editable"); setSelectionInHtmlField(); await insertText(htmlEditor, "test"); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("testfirst"); expect(`.o_form_button_cancel`).toBeVisible(); await contains(`.o_form_button_cancel`).click(); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("first"); expect(`.o_form_button_cancel`).not.toBeVisible(); }); test("undo after discard html field changes in form", async () => { await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable p").toHaveText("first"); expect(`.o_form_button_save`).not.toBeVisible(); // move the hoot focus in the editor await click(".odoo-editor-editable"); setSelectionInHtmlField(); await insertText(htmlEditor, "test"); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("testfirst"); expect(`.o_form_button_cancel`).toBeVisible(); await press(["ctrl", "z"]); expect(".odoo-editor-editable p").toHaveText("tesfirst"); expect(`.o_form_button_cancel`).toBeVisible(); await contains(`.o_form_button_cancel`).click(); await animationFrame(); expect(".odoo-editor-editable p").toHaveText("first"); expect(`.o_form_button_cancel`).not.toBeVisible(); await press(["ctrl", "z"]); expect(".odoo-editor-editable p").toHaveText("first"); expect(`.o_form_button_cancel`).not.toBeVisible(); }); test("A new MediaDialog after switching record in a Form view should have the correct resId", async () => { patchWithCleanup(MediaDialog.prototype, { setup() { expect.step(`${this.props.resModel} : ${this.props.resId}`); this.size = "xl"; this.contentClass = "o_select_media_dialog"; this.title = "TEST"; this.tabs = []; this.state = {}; // no call to super to avoid services dependencies // this test only cares about the props given to the dialog }, }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable p:contains(first)").toHaveCount(1); await contains(`.o_pager_next`).click(); expect(".odoo-editor-editable p:contains(second)").toHaveCount(1); setSelectionInHtmlField(); await insertText(htmlEditor, "/Media"); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); expect(".active .o-we-command-name").toHaveText("Media"); await press("Enter"); await animationFrame(); expect.verifySteps(["partner : 2"]); }); test("Embed video by pasting video URL", async () => { Partner._records = [ { id: 1, txt: "


", }, ]; onRpc("/html_editor/video_url/data", async () => { return { platform: "youtube", embed_url: "//www.youtube.com/embed/qxb74CMR748?rel=0&autoplay=0", }; }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); const anchorNode = setSelectionInHtmlField(); // Paste a video URL. pasteText(htmlEditor, "https://www.youtube.com/watch?v=qxb74CMR748"); await animationFrame(); expect(anchorNode.outerHTML).toBe("

https://www.youtube.com/watch?v=qxb74CMR748

"); expect(".o-we-powerbox").toHaveCount(1); expect(queryAllTexts(".o-we-command-name")).toEqual(["Embed Youtube Video", "Paste as URL"]); // Press Enter to select first option in the powerbox ("Embed Youtube Video"). await press("Enter"); await animationFrame(); expect(anchorNode.outerHTML).toBe("

"); expect( 'div.media_iframe_video iframe[src="//www.youtube.com/embed/qxb74CMR748?rel=0&autoplay=0"]' ).toHaveCount(1); }); test("isDirty should be false when the content is being transformed by the editor", async () => { Partner._records = [ { id: 1, txt: "

abc

", }, ]; await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect(`[name='txt'] .odoo-editor-editable`).toHaveInnerHTML("

abc

", { message: "value should be sanitized by the editor", }); expect(`.o_form_button_save`).not.toBeVisible(); }); test.tags("desktop"); test("link preview in Link Popover", async () => { Partner._records = [ { id: 1, txt: "

abcThis website

", }, ]; await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect(".test_target a").toHaveText("This website"); // Open the popover option to edit the link setSelectionInHtmlField(".test_target a"); await animationFrame(); // Click on the edit link icon await contains("a.o_we_edit_link").click(); expect(".o-we-linkpopover input.o_we_label_link").toHaveValue("This website", { message: "The label input field should match the link's content", }); expect(".o-we-linkpopover a#link-preview").toHaveText("This website", { message: "Link label in preview should match label input field", }); await contains(".o-we-linkpopover input.o_we_label_link").edit("Bad new label"); expect(".o-we-linkpopover input.o_we_label_link").toHaveValue("Bad new label", { message: "The label input field should match the link's content", }); expect(".o-we-linkpopover a#link-preview").toHaveText("Bad new label", { message: "Link label in preview should match label input field", }); // Move selection outside to discard setSelectionInHtmlField(".test_target"); await waitUntil(() => !document.querySelector(".o-we-linkpopover"), { timeout: 500 }); expect(".o-we-linkpopover").toHaveCount(0); expect(".test_target a").toHaveText("This website"); // Select link label to open the floating toolbar. setSelectionInHtmlField(".test_target a"); await animationFrame(); // Click on the edit link icon await contains("a.o_we_edit_link").click(); expect(".o-we-linkpopover input.o_we_label_link").toHaveValue("This website", { message: "The label input field should match the link's content", }); expect(".o-we-linkpopover a#link-preview").toHaveText("This website", { message: "Link label in preview should match label input field", }); // Open the popover option to edit the link await contains(".o-we-linkpopover input.o_we_label_link").edit("New label"); expect(".o-we-linkpopover a#link-preview").toHaveText("New label", { message: "Preview should be updated on label input field change", }); // Click "Save". await contains(".o-we-linkpopover .o_we_apply_link").click(); expect(".test_target a").toHaveText("New label", { message: "The link's label should be updated", }); }); test("html field with a placeholder", async () => { Partner._records = [ { id: 1, txt: false, }, ]; await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect(`[name="txt"] .odoo-editor-editable`).toHaveInnerHTML( '


', { type: "html" } ); setSelectionInHtmlField(); await tick(); expect(`[name="txt"] .odoo-editor-editable`).toHaveInnerHTML( '


', { type: "html" } ); moveSelectionOutsideEditor(); await tick(); expect(`[name="txt"] .odoo-editor-editable`).toHaveInnerHTML( '


', { type: "html" } ); }); test("'Video Link' command is available", async () => { await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "/video"); await waitFor(".o-we-powerbox"); expect(queryAllTexts(".o-we-command-name")).toEqual(["Video Link"]); }); test("MediaDialog contains 'Videos' tab by default in html field", async () => { await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "/media"); await waitFor(".o-we-powerbox"); expect(queryAllTexts(".o-we-command-name")[0]).toBe("Media"); await press("Enter"); await animationFrame(); expect(queryAllTexts(".o_select_media_dialog .nav-tabs .nav-item")).toEqual([ "Images", "Documents", "Icons", "Videos", ]); }); test("MediaDialog does not contain 'Videos' tab in html field when 'disableVideo' = true", async () => { await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "/media"); await waitFor(".o-we-powerbox"); expect(queryAllTexts(".o-we-command-name")[0]).toBe("Media"); await press("Enter"); await animationFrame(); expect(queryAllTexts(".o_select_media_dialog .nav-tabs .nav-item")).toEqual([ "Images", "Documents", "Icons", ]); }); test("MediaDialog does not contain 'Videos' tab when sanitize = true", async () => { class SanitizePartner extends models.Model { _name = "sanitize.partner"; txt = fields.Html({ sanitize: true }); _records = [{ id: 1, txt: "

first sanitize

" }]; } defineModels([SanitizePartner]); await mountView({ type: "form", resId: 1, resModel: "sanitize.partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "/media"); await waitFor(".o-we-powerbox"); expect(queryAllTexts(".o-we-command-name")[0]).toBe("Media"); await press("Enter"); await animationFrame(); expect(queryAllTexts(".o_select_media_dialog .nav-tabs .nav-item")).toEqual([ "Images", "Documents", "Icons", ]); }); test("MediaDialog contains 'Videos' tab when sanitize_tags = true and 'disableVideo' = false", async () => { class SanitizePartner extends models.Model { _name = "sanitize.partner"; txt = fields.Html({ sanitize_tags: true }); _records = [{ id: 1, txt: "

first sanitize tags

" }]; } defineModels([SanitizePartner]); await mountView({ type: "form", resId: 1, resModel: "sanitize.partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "/media"); await waitFor(".o-we-powerbox"); expect(queryAllTexts(".o-we-command-name")[0]).toBe("Media"); await press("Enter"); await animationFrame(); expect(queryAllTexts(".o_select_media_dialog .nav-tabs .nav-item")).toEqual([ "Images", "Documents", "Icons", "Videos", ]); }); test("'Media' command is available by default", async () => { await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "/media"); await waitFor(".o-we-powerbox"); expect(queryAllTexts(".o-we-command-name")[0]).toBe("Media"); }); test("'Media' command is not available when 'disableImage' = true", async () => { await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "/media"); await animationFrame(); expect(queryAllTexts(".o-we-command-name")).not.toInclude("Media"); }); test("codeview is not available by default", async () => { await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); const node = queryOne(".odoo-editor-editable p"); setSelection({ anchorNode: node, anchorOffset: 0, focusNode: node, focusOffset: 1 }); await waitFor(".o-we-toolbar"); expect(".o-we-toolbar button[name='codeview']").toHaveCount(0); }); test("codeview is not available when not in debug mode", async () => { patchWithCleanup(odoo, { debug: false }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); const node = queryOne(".odoo-editor-editable p"); setSelection({ anchorNode: node, anchorOffset: 0, focusNode: node, focusOffset: 1 }); await waitFor(".o-we-toolbar"); expect(".o-we-toolbar button[name='codeview']").toHaveCount(0); }); test("codeview is available when option is active and in debug mode", async () => { patchWithCleanup(odoo, { debug: true }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); const node = queryOne(".odoo-editor-editable p"); setSelection({ anchorNode: node, anchorOffset: 0, focusNode: node, focusOffset: 1 }); await waitFor(".o-we-toolbar"); expect(".o-we-toolbar button[name='codeview']").toHaveCount(1); }); test("enable/disable codeview with editor toolbar", async () => { patchWithCleanup(odoo, { debug: true }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

first

"); expect("[name='txt'] textarea").toHaveCount(0); // Switch to code view const node = queryOne(".odoo-editor-editable p"); setSelection({ anchorNode: node, anchorOffset: 0, focusNode: node, focusOffset: 1 }); await waitFor(".o-we-toolbar"); await contains(".o-we-toolbar button[name='codeview']").click(); expect("[name='txt'] .odoo-editor-editable").toHaveClass("d-none"); expect("[name='txt'] textarea").toHaveValue("

first

"); // Switch to editor await contains(".o_codeview_btn").click(); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

first

"); expect("[name='txt'] textarea").toHaveCount(0); }); test("edit and enable/disable codeview with editor toolbar", async () => { patchWithCleanup(odoo, { debug: true }); onRpc("partner", "web_save", ({ args }) => { expect(args[1].txt).toBe("
"); expect.step("web_save"); }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "Hello "); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Hello first

"); // Switch to code view const node = queryOne(".odoo-editor-editable p"); setSelection({ anchorNode: node, anchorOffset: 0, focusNode: node, focusOffset: 1 }); await waitFor(".o-we-toolbar"); await contains(".o-we-toolbar button[name='codeview']").click(); expect("[name='txt'] textarea").toHaveValue("

Hello first

"); await contains("[name='txt'] textarea").edit("

Yop

"); expect("[name='txt'] textarea").toHaveValue("

Yop

"); // Switch to editor await contains(".o_codeview_btn").click(); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Yop

"); undo(htmlEditor); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Hello first

"); undo(htmlEditor); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Hellofirst

"); }); test("edit and save a html field in collaborative should keep the same wysiwyg", async () => { patchWithCleanup(Wysiwyg.prototype, { setup() { super.setup(); expect.step("Setup Wysiwyg"); }, }); onRpc("partner", "web_save", ({ args }) => { const txt = args[1].txt; expect(normalizeHTML(txt, stripHistoryIds)).toBe("

Hello first

"); expect.step("web_save"); args[1].txt = txt.replace( /\sdata-last-history-steps="[^"]*?"/, ' data-last-history-steps="12345"' ); }); onRpc("/html_editor/get_ice_servers", () => { return []; }); onRpc("/html_editor/bus_broadcast", (params) => { return { id: 10 }; }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(); await insertText(htmlEditor, "Hello "); expect("[name='txt'] .odoo-editor-editable").toHaveInnerHTML("

Hello first

"); expect.verifySteps(["Setup Wysiwyg"]); await clickSave(); expect.verifySteps(["web_save"]); }); describe("sandbox", () => { const recordWithComplexHTML = { id: 1, txt: ` Hello `, }; function getSandboxContent(content) { return ` ${content} `; } test("complex html is automatically in sandboxed preview mode", async () => { Partner._records = [recordWithComplexHTML]; await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect( `.o_field_html[name="txt"] iframe[sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"]` ).toHaveCount(1); }); test("readonly sandboxed preview", async () => { Partner._records = [recordWithComplexHTML]; await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); const readonlyIframe = queryOne( '.o_field_html[name="txt"] iframe[sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"]' ); expect(readonlyIframe.contentDocument.body).toHaveText("Hello"); expect( readonlyIframe.contentWindow.getComputedStyle(readonlyIframe.contentDocument.body).color ).toBe("rgb(0, 0, 255)"); expect("#codeview-btn-group > button").toHaveCount(0, { message: "Codeview toggle should not be possible in readonly mode.", }); }); function htmlDocumentTextTemplate(text, color) { return ` ${text} `; } test("sandboxed preview display and editing", async () => { Partner._records = [ { id: 1, txt: htmlDocumentTextTemplate("Hello", "red"), }, ]; onRpc("partner", "web_save", ({ args }) => { expect(args[1].txt).toBe(htmlDocumentTextTemplate("Hi", "blue")); expect.step("web_save"); }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); // check original displayed content let iframe = queryOne( '.o_field_html[name="txt"] iframe[sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"]' ); expect(`.o_form_button_save`).not.toBeVisible(); expect(iframe.contentDocument.body).toHaveText("Hello"); expect( iframe.contentDocument.head.querySelector("style").textContent.trim().replace(/\s/g, "") ).toBe("body{color:red;}", { message: "Head nodes should remain unaltered in the head", }); expect(iframe.contentWindow.getComputedStyle(iframe.contentDocument.body).color).toBe( "rgb(255, 0, 0)" ); expect("#codeview-btn-group > button").toHaveCount(1); // switch to XML editor and edit await contains("#codeview-btn-group > button").click(); expect('.o_field_html[name="txt"] textarea').toHaveCount(1); await contains('.o_field_html[name="txt"] textarea').edit( htmlDocumentTextTemplate("Hi", "blue") ); expect(`.o_form_button_save`).toBeVisible(); // check displayed content after edit await contains("#codeview-btn-group > button").click(); iframe = queryOne( '.o_field_html[name="txt"] iframe[sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"]' ); await animationFrame(); expect(iframe.contentDocument.body).toHaveText("Hi"); expect( iframe.contentDocument.head.querySelector("style").textContent.trim().replace(/\s/g, "") ).toBe("body{color:blue;}", { message: "Head nodes should remain unaltered in the head", }); expect(iframe.contentWindow.getComputedStyle(iframe.contentDocument.body).color).toBe( "rgb(0, 0, 255)" ); await contains(".o_form_button_save").click(); expect.verifySteps(["web_save"]); }); test("switch page after editing html with code editor", async () => { Partner._records = [ { id: 1, txt: htmlDocumentTextTemplate("Hello", "red"), }, { id: 2, txt: htmlDocumentTextTemplate("Bye", "green"), }, ]; onRpc("partner", "web_save", ({ args }) => { expect(args[1].txt).toBe(htmlDocumentTextTemplate("Hi", "blue")); expect.step("web_save"); }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); // switch to XML editor and edit await contains("#codeview-btn-group > button").click(); expect('.o_field_html[name="txt"] textarea').toHaveValue( htmlDocumentTextTemplate("Hello", "red") ); await contains('.o_field_html[name="txt"] textarea').edit( htmlDocumentTextTemplate("Hi", "blue") ); expect(`.o_form_button_save`).toBeVisible(); expect('.o_field_html[name="txt"] textarea').toHaveValue( htmlDocumentTextTemplate("Hi", "blue") ); await contains(`.o_pager_next`).click(); expect.verifySteps(["web_save"]); expect(`.o_form_button_save`).not.toBeVisible(); expect('.o_field_html[name="txt"] textarea').toHaveValue( htmlDocumentTextTemplate("Bye", "green") ); await contains(`.o_pager_previous`).click(); expect('.o_field_html[name="txt"] textarea').toHaveValue( htmlDocumentTextTemplate("Hi", "blue") ); }); test("sanboxed preview mode not automatically enabled for regular values", async () => { Partner._records = [ { id: 1, txt: `

Hello

`, }, ]; await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect(`.o_field_html[name="txt"] iframe[sandbox]`).toHaveCount(0); expect(`.o_field_html[name="txt"] iframe[sandbox]`).toHaveCount(0); }); test("sandboxed preview option applies even for simple text", async () => { Partner._records = [ { id: 1, txt: ` Hello `, }, ]; await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect( `.o_field_html[name="txt"] iframe[sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"]` ).toHaveCount(1); }); test("links should open on a new tab in sandboxedPreview", async () => { Partner._records = [ { id: 1, txt: getSandboxContent(` `), }, { id: 2, txt: getSandboxContent(` `), }, ]; await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); let readonlyIframe = queryOne('.o_field_html[name="txt"] iframe'); expect(readonlyIframe.contentDocument.body.querySelector("p")).toHaveText("first"); for (const link of readonlyIframe.contentDocument.body.querySelectorAll("a")) { expect(link.getAttribute("target")).toBe("_blank"); expect(link.getAttribute("rel")).toBe("noreferrer"); } await contains(`.o_pager_next`).click(); readonlyIframe = queryOne('.o_field_html[name="txt"] iframe'); expect(readonlyIframe.contentDocument.body.querySelector("p")).toHaveText("second"); for (const link of readonlyIframe.contentDocument.body.querySelectorAll("a")) { expect(link.getAttribute("target")).toBe("_blank"); expect(link.getAttribute("rel")).toBe("noreferrer"); } }); test("html field in readonly updated by onchange in sandboxedPreview", async () => { Partner._records = [{ id: 1, name: "first", txt: getSandboxContent("

first

") }]; Partner._onChanges = { name(record) { record.txt = getSandboxContent(`

${record.name}

`); }, }; await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); let readonlyIframe = queryOne('.o_field_html[name="txt"] iframe'); expect(readonlyIframe.contentDocument.body).toHaveInnerHTML(`

first

`); await contains(`.o_field_widget[name=name] input`).edit("hello"); readonlyIframe = queryOne('.o_field_html[name="txt"] iframe'); expect(readonlyIframe.contentDocument.body).toHaveInnerHTML(`

hello

`); }); test("readonly with cssReadonly", async () => { Partner._records = [ { id: 1, txt: `

Hello

`, }, ]; patchWithCleanup(assets, { async getBundle(name) { expect.step(name); return { cssLibs: ["testCSS"], jsLibs: [], }; }, }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); const readonlyIframe = queryOne('.o_field_html[name="txt"] iframe'); expect( readonlyIframe.contentDocument.head.querySelector(`link[href='testCSS']`) ).toHaveCount(1); expect(readonlyIframe.contentDocument.body).toHaveInnerHTML( `

Hello

` ); expect.verifySteps(["template.assets"]); }); test("click on next/previous page when readonly with cssReadonly ", async () => { await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); let readonlyIframe = queryOne('.o_field_html[name="txt"] iframe'); expect(readonlyIframe.contentDocument.body).toHaveInnerHTML( `

first

` ); await contains(`.o_pager_next`).click(); readonlyIframe = queryOne('.o_field_html[name="txt"] iframe'); expect(readonlyIframe.contentDocument.body).toHaveInnerHTML( `

second

` ); }); }); describe("direction config", () => { test("ltr direction", async () => { defineParams({ lang_parameters: { direction: "ltr", }, }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable").toHaveAttribute("dir", "ltr"); const node = queryOne(".odoo-editor-editable p"); setSelection({ anchorNode: node.firstChild, anchorOffset: 0 }); await insertText(htmlEditor, "/Switchdirection"); await animationFrame(); expect(queryAllTexts(".o-we-command-name")).toEqual(["Switch direction"]); await press("Enter"); expect(".odoo-editor-editable p").toHaveAttribute("dir", "rtl"); }); test("rtl direction", async () => { defineParams({ lang_parameters: { direction: "rtl", }, }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); expect(".odoo-editor-editable").toHaveAttribute("dir", "rtl"); const node = queryOne(".odoo-editor-editable p"); setSelection({ anchorNode: node.firstChild, anchorOffset: 0 }); await insertText(htmlEditor, "/Switchdirection"); await animationFrame(); expect(queryAllTexts(".o-we-command-name")).toEqual(["Switch direction"]); await press("Enter"); expect(".odoo-editor-editable p").toHaveAttribute("dir", "ltr"); }); }); describe("save image", () => { function pasteFile(editor, file) { const clipboardData = new DataTransfer(); clipboardData.items.add(file); const pasteEvent = new ClipboardEvent("paste", { clipboardData, bubbles: true }); editor.editable.dispatchEvent(pasteEvent); } function createBase64ImageFile(base64ImageData) { const binaryImageData = atob(base64ImageData); const uint8Array = new Uint8Array(binaryImageData.length); for (let i = 0; i < binaryImageData.length; i++) { uint8Array[i] = binaryImageData.charCodeAt(i); } return new File([uint8Array], "test_image.png", { type: "image/png" }); } test("Ensure that urgentSave works even with modified image to save", async () => { expect.assertions(5); Partner._records = [ { id: 1, txt: "


", }, ]; let sendBeaconDef; mockSendBeacon((route, blob) => { blob.text().then((r) => { const { params } = JSON.parse(r); const { args, model } = params; if (route === "/web/dataset/call_kw/partner/web_save" && model === "partner") { if (writeCount === 0) { // Save normal value without image. expect(args[1].txt).toBe(`

a

`); } else if (writeCount === 1) { // Save image with unfinished modification changes. expect(args[1].txt).toBe(imageContainerHTML); } else if (writeCount === 2) { // Save the modified image. expect(args[1].txt).toBe(getImageContainerHTML(newImageSrc, false)); } else { // Fail the test if too many write are called. expect(true).toBe("false"); throw new Error("Write should only be called 3 times during this test"); } writeCount += 1; } sendBeaconDef.resolve(); }); return true; }); let formController; // Patch to get the controller instance. patchWithCleanup(FormController.prototype, { setup() { super.setup(...arguments); formController = this; }, }); const imageRecord = IrAttachment._records[0]; // Method to get the html of a cropped image. const getImageContainerHTML = (src, isModified) => { return `


` .replace(/(?:\s|(?:\r\n))+/g, " ") .replace(/\s?(<|>)\s?/g, "$1"); }; // Promise to resolve when we want the response of the modify_image RPC. const modifyImagePromise = new Deferred(); let writeCount = 0; let modifyImageCount = 0; // Valid base64 encoded image in its transitory modified state. const imageContainerHTML = getImageContainerHTML( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII", true ); // New src URL to assign to the image when the modification is // "registered". const newImageSrc = "/web/image/1234/cropped_transparent.png"; onRpc("web_save", () => { expect(true).toBe(false); throw new Error("web_save should only be called through sendBeacon"); }); onRpc(`/html_editor/modify_image/${imageRecord.id}`, async (request) => { if (modifyImageCount === 0) { const { params } = await request.json(); expect(params.res_model).toBe("partner"); expect(params.res_id).toBe(1); await modifyImagePromise; modifyImageCount++; return newImageSrc; } else { // Fail the test if too many modify_image are called. expect(true).toBe(false); throw new Error("The image should only have been modified once during this test"); } }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); // Simulate an urgent save without any image in the content. sendBeaconDef = new Deferred(); setSelectionInHtmlField(".test_target"); await insertText(htmlEditor, "a"); htmlEditor.shared.history.addStep(); await formController.beforeUnload(); await sendBeaconDef; // Replace the empty paragraph with a paragrah containing an unsaved // modified image const imageContainerElement = parseHTML(htmlEditor.document, imageContainerHTML).firstChild; const paragraph = htmlEditor.editable.querySelector(".test_target"); htmlEditor.editable.replaceChild(imageContainerElement, paragraph); htmlEditor.shared.history.addStep(); // Simulate an urgent save before the end of the RPC roundtrip for the // image. sendBeaconDef = new Deferred(); await formController.beforeUnload(); await sendBeaconDef; // Resolve the image modification (simulate end of RPC roundtrip). modifyImagePromise.resolve(); await modifyImagePromise; await animationFrame(); // Simulate the last urgent save, with the modified image. sendBeaconDef = new Deferred(); await formController.beforeUnload(); await sendBeaconDef; }); test("Pasted/dropped images are converted to attachments on save", async () => { Partner._records = [ { id: 1, txt: "


", }, ]; onRpc("/html_editor/attachment/add_data", async (request) => { const { params } = await request.json(); const { res_id, res_model } = params; expect.step(`add_data: ${res_model} ${res_id}`); return { image_src: "/test_image_url.png", access_token: "1234", public: false, }; }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(".test_target"); // Paste image. pasteFile( htmlEditor, createBase64ImageFile( "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" ) ); await waitFor("img"); const img = htmlEditor.editable.querySelector("img"); expect(img.src.startsWith("data:image/png;base64,")).toBe(true); expect(img).toHaveClass("o_b64_image_to_save"); // Save changes. await contains(".o_form_button_save").click(); expect(img.getAttribute("src")).toBe("/test_image_url.png?access_token=1234"); expect(img).not.toHaveClass("o_b64_image_to_save"); expect.verifySteps(["add_data: partner 1"]); }); test("Pasted/dropped images are converted once to attachments on save with slow network", async () => { Partner._records = [ { id: 1, txt: "


", }, ]; const def = new Deferred(); onRpc("/html_editor/attachment/add_data", async (request) => { const { params } = await request.json(); const { res_id, res_model } = params; expect.step(`add_data-start: ${res_model} ${res_id}`); await def; expect.step(`add_data-end: ${res_model} ${res_id}`); return { image_src: "/test_image_url.png", access_token: "1234", public: false, }; }); onRpc("partner", "web_save", ({ args }) => { expect.step("web_save"); expect(args[1].txt).toBe( `

` ); }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(".test_target"); // Paste image. pasteFile( htmlEditor, createBase64ImageFile( "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" ) ); await waitFor("img"); const img = htmlEditor.editable.querySelector("img"); expect(img.src.startsWith("data:image/png;base64,")).toBe(true); expect(img).toHaveClass("o_b64_image_to_save"); // Save changes. await contains(".o_form_button_save").click(); expect(img.src.startsWith("data:image/png;base64,")).toBe(true); expect(img).toHaveClass("o_b64_image_to_save"); def.resolve(); await tick(); expect(img.getAttribute("src")).toBe("/test_image_url.png?access_token=1234"); expect(img).not.toHaveClass("o_b64_image_to_save"); expect.verifySteps(["add_data-start: partner 1", "add_data-end: partner 1", "web_save"]); }); test("Pasted/dropped images are converted once to attachments on switch page with slow network", async () => { Partner._records = [ { id: 1, txt: "


", }, { id: 2, txt: "


", }, ]; const def = new Deferred(); onRpc("/html_editor/attachment/add_data", async (request) => { const { params } = await request.json(); const { res_id, res_model } = params; expect.step(`add_data-start: ${res_model} ${res_id}`); await def; expect.step(`add_data-end: ${res_model} ${res_id}`); return { image_src: "/test_image_url.png", access_token: "1234", public: false, }; }); onRpc("partner", "web_save", ({ args }) => { expect.step("web_save"); expect(args[1].txt).toBe( `

` ); }); await mountView({ type: "form", resId: 1, resIds: [1, 2], resModel: "partner", arch: `
`, }); setSelectionInHtmlField(".test_target"); // Paste image. pasteFile( htmlEditor, createBase64ImageFile( "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" ) ); await waitFor("img"); const img = htmlEditor.editable.querySelector("img"); expect(img.src.startsWith("data:image/png;base64,")).toBe(true); expect(img).toHaveClass("o_b64_image_to_save"); // Save changes. await contains(".o_pager_next").click(); expect(img.src.startsWith("data:image/png;base64,")).toBe(true); expect(img).toHaveClass("o_b64_image_to_save"); expect(".test_target_2").toHaveCount(0); def.resolve(); await animationFrame(); expect(".test_target_2").toHaveCount(1); expect.verifySteps(["add_data-start: partner 1", "add_data-end: partner 1", "web_save"]); }); test("Pasted/dropped images are converted to attachments without access_token on save", async () => { Partner._records = [ { id: 1, txt: "


", }, ]; onRpc("/html_editor/attachment/add_data", async (request) => { const { params } = await request.json(); const { res_id, res_model } = params; expect.step(`add_data: ${res_model} ${res_id}`); return { image_src: "/test_image_url.png", id: 123, public: false, }; }); onRpc("ir.attachment", "generate_access_token", ({ args }) => { expect.step(`generate_access_token: ${args}`); return ["12345"]; }); await mountView({ type: "form", resId: 1, resModel: "partner", arch: `
`, }); setSelectionInHtmlField(".test_target"); // Paste image. pasteFile( htmlEditor, createBase64ImageFile( "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" ) ); await waitFor("img"); const img = htmlEditor.editable.querySelector("img"); expect(img.src.startsWith("data:image/png;base64,")).toBe(true); expect(img).toHaveClass("o_b64_image_to_save"); // Save changes. await contains(".o_form_button_save").click(); expect(img.getAttribute("src")).toBe("/test_image_url.png?access_token=12345"); expect(img).not.toHaveClass("o_b64_image_to_save"); expect.verifySteps(["add_data: partner 1", "generate_access_token: 123"]); }); }); describe("translatable", () => { test("should display translate button when html field is translatable", async () => { Partner._fields.txt = fields.Html({ string: "txt", translate: true }); serverState.lang = "en_US"; serverState.multiLang = true; await mountView({ type: "form", resModel: "partner", resId: 1, arch: /* xml */ `
`, }); expect(".o_field_html .btn.o_field_translate").not.toBeVisible(); // Focus on the editable to make the translate button visible await contains(".odoo-editor-editable").click(); expect(".o_field_html .btn.o_field_translate").toBeVisible(); // Click away to remove focus await contains(".o_form_label").click(); expect(".o_field_html .btn.o_field_translate").not.toBeVisible(); }); });