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`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
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": `${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: "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( '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
first
second
a