replace stale web_editor with html_editor and html_builder for 19.0

web_editor was removed in Odoo 19.0 and replaced by html_editor
and html_builder. The old web_editor was incorrectly included in
the 19.0 vanilla import.

🤖 assisted by claude
This commit is contained in:
Ernad Husremovic 2026-03-09 15:31:13 +01:00
parent 4b94f0abc5
commit f866779561
1513 changed files with 396049 additions and 358525 deletions

View file

@ -0,0 +1,998 @@
import { expect, test } from "@odoo/hoot";
import {
manuallyDispatchProgrammaticEvent,
click,
press,
queryOne,
waitFor,
waitForNone,
} from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { contains, makeMockEnv, onRpc } from "@web/../tests/web_test_helpers";
import { CaptionPlugin } from "@html_editor/others/embedded_components/plugins/caption_plugin/caption_plugin";
import { MAIN_PLUGINS, EMBEDDED_COMPONENT_PLUGINS } from "@html_editor/plugin_sets";
import { MAIN_EMBEDDINGS } from "@html_editor/others/embedded_components/embedding_sets";
import { closestElement } from "@html_editor/utils/dom_traversal";
import { setupEditor, testEditor } from "./_helpers/editor";
import { unformat } from "./_helpers/format";
import { deleteBackward, deleteForward, insertText } from "./_helpers/user_actions";
import { cleanHints } from "./_helpers/dispatch";
import { getContent } from "./_helpers/selection";
import { expectElementCount } from "./_helpers/ui_expectations";
class CaptionPluginWithPredictableId extends CaptionPlugin {
getCaptionId() {
return "1";
}
}
const base64Img =
"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA\n AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO\n 9TXL0Y4OHwAAAABJRU5ErkJggg==";
const configWithEmbeddedCaption = {
Plugins: [
...MAIN_PLUGINS,
CaptionPluginWithPredictableId,
...EMBEDDED_COMPONENT_PLUGINS.filter((plugin) => plugin.id !== "caption"),
],
resources: {
embedded_components: [
CaptionPluginWithPredictableId,
...MAIN_EMBEDDINGS.filter((plugin) => plugin.id !== "caption"),
],
},
};
const setupEditorWithEmbeddedCaption = async (content) =>
await setupEditor(content, { config: configWithEmbeddedCaption });
const toggleCaption = async (captionText) => {
await click("img");
await waitFor(".o-we-toolbar button[name='image_caption']");
await click("button[name='image_caption']");
if (captionText) {
await waitFor("figure > figcaption > input");
for (const char of captionText) {
if (char.toUpperCase() === char) {
await press(["Shift", char]);
} else {
await press(char);
}
}
const input = queryOne("input");
expect(input.value).toBe("Hello");
}
};
const addLinkToImage = async (url) => {
await click("img");
await waitFor(".o-we-toolbar button[name='link']");
await click(".o-we-toolbar");
await click("button[name='link']");
if (url) {
await waitFor(".o-we-linkpopover");
await contains(".o-we-linkpopover input.o_we_href_input_link", { timeout: 1500 }).edit(
"odoo.com"
);
}
};
const removeLinkFromImage = async () => {
await click("img");
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar");
await click("button[name='unlink']");
};
const objectToAttributesString = (attributes) =>
Object.entries(attributes)
.map(([k, v]) => (v.includes('"') ? `${k}='${v}'` : `${k}="${v}"`))
.join(" ");
const getFigcaptionAttributes = (captionId, caption = "", focusInput = false) => {
const attributes = {
"data-embedded": "caption",
"data-oe-protected": "true",
contenteditable: "false",
class: "mt-2",
"data-embedded-props": `{"id":"${captionId}","focusInput":${focusInput}}`,
};
if (caption) {
attributes.placeholder = caption;
}
return objectToAttributesString(attributes);
};
const CAPTION_INPUT_ATTRIBUTES = objectToAttributesString({
type: "text",
maxlength: "100",
class: "border-0 p-0",
placeholder: "Write your caption here",
});
test.tags("focus required");
test("add a caption to an image and focus it", async () => {
const captionId = 1;
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<img class="img-fluid test-image" src="${base64Img}">`,
stepFunction: async (editor) => {
await toggleCaption();
await waitFor("figcaption > input");
const input = queryOne("figure > figcaption > input");
expect(input.value).toBe("");
expect(editor.document.activeElement).toBe(input);
expect(editor.document.getSelection().anchorNode.nodeName).toBe("FIGCAPTION");
// Remove the editor selection for the test because it's irrelevant
// since the focus is not in it.
const selection = editor.document.getSelection();
selection.removeAllRanges();
cleanHints(editor);
},
contentAfterEdit: unformat(
`<p><br></p>
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption-id="${captionId}" data-caption="">
<figcaption ${getFigcaptionAttributes(captionId, "", true)}>
<input ${CAPTION_INPUT_ATTRIBUTES}>
</figcaption>
</figure>
<p><br></p>`
),
});
});
test.tags("focus required");
test("add a caption to an image surrounded by text and focus it", async () => {
const captionId = 1;
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<p>ab<img class="img-fluid test-image" src="${base64Img}">cd</p>`,
stepFunction: async (editor) => {
await toggleCaption();
await waitFor("figcaption > input");
const input = queryOne("figure > figcaption > input");
expect(input.value).toBe("");
expect(editor.document.activeElement).toBe(input);
// Remove the editor selection for the test because it's irrelevant
// since the focus is not in it.
const selection = editor.document.getSelection();
selection.removeAllRanges();
},
contentAfterEdit: unformat(
`<p>ab</p>
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption-id="${captionId}" data-caption="">
<figcaption ${getFigcaptionAttributes(captionId, "", true)}>
<input ${CAPTION_INPUT_ATTRIBUTES}>
</figcaption>
</figure>
<p>cd</p>`
),
});
});
test("saving an image with a caption replaces the input with plain text", async () => {
const captionId = 1;
const caption = "Hello";
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>${caption}</figcaption>
</figure>`
),
contentBeforeEdit: unformat(
// Paragraphs get added to ensure we can write before/after the figure.
`<p><br></p>
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption-id="${captionId}" data-caption="${caption}">
<figcaption ${getFigcaptionAttributes(captionId, caption)}>
<input ${CAPTION_INPUT_ATTRIBUTES}>
</figcaption>
</figure>
<p><br></p>`
),
// Unchanged.
contentAfterEdit: unformat(
`<p><br></p>
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption-id="${captionId}" data-caption="${caption}">
<figcaption ${getFigcaptionAttributes(captionId, caption)}>
<input ${CAPTION_INPUT_ATTRIBUTES}>
</figcaption>
</figure>
<p><br></p>`
),
// Cleaned up for screen readers.
contentAfter: unformat(
`<p><br></p>
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>
${caption}
</figcaption>
</figure>
<p><br></p>`
),
});
});
test.tags("focus required");
test("loading an image with a caption embeds it", async () => {
const { editor } = await setupEditorWithEmbeddedCaption(`
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>Hello</figcaption>
</figure>
`);
const image = queryOne("img");
expect(image.getAttribute("data-caption")).toBe("Hello");
const input = queryOne("figure > figcaption > input");
expect(input.value).toBe("Hello");
// Do not focus the input when loading the page.
expect(editor.document.activeElement).not.toBe(input);
});
test.tags("focus required");
test("clicking the caption button on an image with a caption removes the caption", async () => {
const caption = "Hello";
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>${caption}</figcaption>
</figure>`
),
stepFunction: async (editor) => {
const input = queryOne("figure > figcaption > input");
await toggleCaption();
expect(editor.document.activeElement).not.toBe(input);
await expectElementCount(".o-we-toolbar", 1);
},
contentAfterEdit: unformat(
`<p><br></p>
<p>[<img class="img-fluid test-image" src="${base64Img}" data-caption="${caption}">]</p>
<p><br></p>`
),
// Unchanged
contentAfter: unformat(
`<p><br></p>
<p>[<img class="img-fluid test-image" src="${base64Img}" data-caption="${caption}">]</p>
<p><br></p>`
),
});
});
test.tags("focus required");
test("leaving the caption persists its value", async () => {
const captionId = 1;
const caption = "Hello";
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<p><img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption="${caption}"></p><h1>Heading</h1>`,
stepFunction: async (editor) => {
await toggleCaption();
await waitFor("figcaption > input");
const input = queryOne("figure > figcaption > input");
expect(editor.document.activeElement).toBe(input);
await press("a");
await press("b");
await press("c");
await press("Backspace");
expect(input.value).toBe(`${caption}ab`);
expect(editor.document.activeElement).toBe(input);
const heading = queryOne("h1");
await click(heading);
expect(editor.document.activeElement).not.toBe(input);
await animationFrame(); // Wait for the selection to change.
},
contentAfterEdit: unformat(
`<p><br></p>
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption="${caption}ab" data-caption-id="${captionId}">
<figcaption ${getFigcaptionAttributes(captionId, caption + "ab", true)}>
<input ${CAPTION_INPUT_ATTRIBUTES}>
</figcaption>
</figure>
<h1>[]Heading</h1>`
),
contentAfter: unformat(
`<p><br></p>
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>
${caption}ab
</figcaption>
</figure>
<h1>[]Heading</h1>`
),
});
});
test.tags("focus required");
test("can't use the powerbox in a caption", async () => {
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<img class="img-fluid test-image" src="${base64Img}"><h1>Heading</h1>`,
stepFunction: async (editor) => {
await toggleCaption();
await waitFor("figcaption > input");
const input = queryOne("figure > figcaption > input");
expect(editor.document.activeElement).toBe(input);
await press("/");
await animationFrame();
await expectElementCount(".o-we-powerbox", 0);
const heading = queryOne("h1");
await click(heading);
await animationFrame(); // Wait for the selection to change.
},
contentAfter: unformat(
`<p><br></p>
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>
/
</figcaption>
</figure>
<h1>[]Heading</h1>`
),
});
});
test.tags("desktop", "focus required");
test("can't use the toolbar in a caption", async () => {
// TODO: The toolbar should not _always_ be usable in mobile!
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption="Hello"><h1>[]Heading</h1>`,
stepFunction: async (editor) => {
await toggleCaption();
await waitFor("figcaption > input");
const input = queryOne("figure > figcaption > input");
expect(editor.document.activeElement).toBe(input);
await animationFrame();
await expectElementCount(".o-we-toolbar", 0);
input.select();
// Check that the contents of the input were indeed selected by
// inserting text.
editor.document.execCommand("insertText", false, "a");
expect(input.value).toBe("a");
await click("h1"); // Blur the input.
await animationFrame(); // Wait for the focus event to trigger a step.
editor.shared.selection.setCursorStart(queryOne("h1"));
},
contentAfter: unformat(
`<p><br></p>
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>a</figcaption>
</figure>
<h1>[]Heading</h1>`
),
});
});
test.tags("focus required");
test("undo in a caption undoes the last caption action then returns to regular editor undo", async () => {
const caption = "Hello";
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<p><img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption="${caption}"></p><h1>[]Heading</h1>`,
stepFunction: async (editor) => {
await insertText(editor, "a");
const heading = queryOne("h1");
expect(heading.textContent).toBe("aHeading");
await toggleCaption();
await waitFor("figcaption > input");
const input = queryOne("figure > figcaption > input");
expect(editor.document.activeElement).toBe(input);
// Using native execCommand so the input's native history works.
await editor.document.execCommand("insertText", false, "b");
await editor.document.execCommand("insertText", false, "c");
await editor.document.execCommand("insertText", false, "d");
await editor.document.execCommand("delete", false, null); // Backspace.
expect(input.value).toBe(`${caption}bc`);
// We simulate undo with Ctrl+Z because we want to see how it
// interacts with native browser behavior.
const ctrlZ = async (target, shouldApplyNativeUndo) => {
const keydown = await manuallyDispatchProgrammaticEvent(target, "keydown", {
key: "z",
ctrlKey: true,
});
if (keydown.defaultPrevented) {
return;
}
let valueBeforeUndo;
if (target === input) {
valueBeforeUndo = input.value;
// This is supposed to happen only after "beforeinput" but
// beforeinput doesn't happen at all if there is nothing to
// undo and this allows us to determine if that is the case.
editor.document.execCommand("undo", false, null);
}
if (shouldApplyNativeUndo) {
// The native undo should have changed the value of the
// input.
expect(input.value).not.toBe(valueBeforeUndo);
} else if (target === input) {
// The native undo should not have changed the value of the input.
expect(input.value).toBe(valueBeforeUndo);
}
if (target !== input || input.value !== valueBeforeUndo) {
// The input events don't get triggered if the input has
// nothing to undo.
const beforeInput = await manuallyDispatchProgrammaticEvent(
target,
"beforeinput",
{
inputType: "historyUndo",
}
);
// --> Here the editor should do its own UNDO.
if (beforeInput.defaultPrevented) {
return;
}
const inputEvent = await manuallyDispatchProgrammaticEvent(target, "input", {
inputType: "historyUndo",
});
if (inputEvent.defaultPrevented) {
return;
}
}
await manuallyDispatchProgrammaticEvent(target, "keyup", {
key: "z",
ctrlKey: true,
});
};
// Native input undo undoes backspace in the input.
expect(editor.document.activeElement).toBe(input);
await ctrlZ(input, true);
expect(input.value).toBe(`${caption}bcd`);
expect(heading.textContent).toBe("aHeading");
// Native input undo undoes all the other key presses in the input.
expect(editor.document.activeElement).toBe(input);
await ctrlZ(input, true);
expect(input.value).toBe(caption);
expect(heading.textContent).toBe("aHeading");
// Editor undo removes the caption.
expect(editor.document.activeElement).toBe(input);
await ctrlZ(input, false);
expect(input.isConnected).toBe(false);
expect(heading.textContent).toBe("aHeading");
// Editor undo removes the key press in the heading.
expect(editor.document.activeElement).not.toBe(input);
const anchor = editor.document.getSelection().anchorNode;
await ctrlZ(closestElement(anchor), false);
expect(heading.textContent).toBe("Heading");
},
contentAfter: unformat(
`<p>
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption="${caption}">
</p>
<h1>[]Heading</h1>`
),
});
});
test("remove an image with a caption", async () => {
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>Hello</figcaption>
</figure>
<h1>[]Heading</h1>`
),
stepFunction: async () => {
await click("img");
await waitFor(".o-we-toolbar button[name='image_delete']");
await click(".o-we-toolbar button[name='image_delete']");
},
contentAfter: unformat(
`<p><br></p>
<h1>[]Heading</h1>`
),
});
});
// For the following two tests.
const getDeleteImageTestData = () => {
const captionId = 1;
const caption = "Hello";
return {
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<p><img class="img-fluid test-image" data-caption="${caption}" src="${base64Img}"></p>
<h1>[]Heading</h1>`
),
prepareImage: async (editor) => {
await toggleCaption();
await waitFor("figcaption > input");
// Check that we indeed have a proper figure structure.
expect(getContent(editor.editable).replace("[]", "")).toBe(
unformat(
`<p><br></p>
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" data-caption="${caption}" src="${base64Img}" data-caption-id="${captionId}">
<figcaption ${getFigcaptionAttributes(
captionId,
caption,
true
)}><input ${CAPTION_INPUT_ATTRIBUTES}></figcaption>
</figure>
<h1>Heading</h1>`
)
);
const input = queryOne("input");
expect(editor.document.activeElement).toBe(input);
expect(input.value).toBe(caption);
// Deselect and reselect the image.
await click("h1");
await click("img");
},
contentAfter: unformat(
`<p><br></p>
<h1>[]Heading</h1>`
),
};
};
test.tags("focus required");
test("remove an image with a caption, using the delete key", async () => {
const { config, contentBefore, prepareImage, contentAfter } = getDeleteImageTestData();
await testEditor({
config,
contentBefore,
stepFunction: async (editor) => {
await prepareImage(editor);
deleteForward(editor);
},
contentAfter,
});
});
test.tags("focus required");
test("remove an image with a caption, using the backspace key", async () => {
const { config, contentBefore, prepareImage, contentAfter } = getDeleteImageTestData();
await testEditor({
config,
contentBefore,
stepFunction: async (editor) => {
await prepareImage(editor);
deleteBackward(editor);
},
contentAfter,
});
});
test("replace an image with a caption", async () => {
onRpc("ir.attachment", "search_read", () => [
{
id: 1,
name: "logo",
mimetype: "image/png",
image_src: "/web/static/img/logo2.png",
access_token: false,
public: true,
},
]);
const env = await makeMockEnv();
await testEditor({
env,
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img src="/web/static/img/logo.png">
<figcaption>Hello</figcaption>
</figure>
<h1>[]Heading</h1>`
),
stepFunction: async () => {
await click("img");
await waitFor(".o-we-toolbar button[name='replace_image']");
await click("button[name='replace_image']");
await waitFor(".o_select_media_dialog");
await click(".o_existing_attachment_cell .o_button_area");
await animationFrame();
expect("img[src='/web/static/img/logo.png']").toHaveCount(0);
expect("img[src='/web/static/img/logo2.png']").toHaveCount(1);
},
// TODO: fix the weird final selection
contentAfter: unformat(
`<p><br></p>
<figure>
<img src="/web/static/img/logo2.png" alt="" data-attachment-id="1" class="img img-fluid o_we_custom_image">
<figcaption>Hello</figcaption>
</figure>
<h1>[]Heading</h1>`
),
});
});
test("edit caption after replacing image", async () => {
onRpc("/web/dataset/call_kw/ir.attachment/search_read", () => [
{
id: 1,
name: "logo",
mimetype: "image/png",
image_src: "/web/static/img/logo2.png",
access_token: false,
public: true,
},
]);
const env = await makeMockEnv();
await testEditor({
env,
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img src="/web/static/img/logo.png">
<figcaption>ab</figcaption>
</figure>
<h1>[]Heading</h1>`
),
stepFunction: async (editor) => {
await click("img");
await waitFor(".o-we-toolbar button[name='replace_image']");
await click("button[name='replace_image']");
await waitFor(".o_select_media_dialog");
await click(".o_existing_attachment_cell .o_button_area");
await animationFrame();
expect("img[src='/web/static/img/logo.png']").toHaveCount(0);
expect("img[src='/web/static/img/logo2.png']").toHaveCount(1);
const input = queryOne("figure > figcaption > input");
await click(input);
expect(editor.document.activeElement).toBe(input);
await press("c");
expect(input.value).toBe("abc");
expect(editor.document.activeElement).toBe(input);
await click("img");
await animationFrame();
},
contentAfter: unformat(
`<p><br></p>
<figure>
[<img src="/web/static/img/logo2.png" alt="" data-attachment-id="1" class="img img-fluid o_we_custom_image">]
<figcaption>abc</figcaption>
</figure>
<h1>Heading</h1>`
),
});
});
test("after replacing a captioned image, undo should revert to the original image and caption", async () => {
onRpc("/web/dataset/call_kw/ir.attachment/search_read", () => [
{
id: 1,
name: "logo",
mimetype: "image/png",
image_src: "/web/static/img/logo2.png",
access_token: false,
public: true,
},
]);
const env = await makeMockEnv();
await testEditor({
env,
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img src="/web/static/img/logo.png" class="img-fluid test-image">
<figcaption></figcaption>
</figure>
<h1>[]Heading</h1>`
),
stepFunction: async () => {
await click("img");
await waitFor(".o-we-toolbar button[name='replace_image']");
await click("button[name='replace_image']");
await waitFor(".o_select_media_dialog");
await click(".o_existing_attachment_cell .o_button_area");
await animationFrame();
expect("img[src='/web/static/img/logo.png']").toHaveCount(0);
expect("img[src='/web/static/img/logo2.png']").toHaveCount(1);
await press(["ctrl", "z"]);
await animationFrame();
expect("img[src='/web/static/img/logo.png']").toHaveCount(1);
expect("img[src='/web/static/img/logo2.png']").toHaveCount(0);
},
contentAfter: unformat(
`<p><br></p>
<figure>
[<img src="/web/static/img/logo.png" class="img-fluid test-image">]
<figcaption></figcaption>
</figure>
<h1>Heading</h1>`
),
});
});
test("add a link to an image with a caption", async () => {
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>Hello</figcaption>
</figure>
<h1>[]Heading</h1>`
),
stepFunction: async () => {
await addLinkToImage("odoo.com");
await expectElementCount(".o-we-linkpopover", 1);
await expectElementCount(".o-we-toolbar", 1);
},
contentAfter: unformat(
`<p><br></p>
<p>
<a href="https://odoo.com">
<figure>
[<img class="img-fluid test-image" src="${base64Img}">]
<figcaption>Hello</figcaption>
</figure>
</a>
</p>
<h1>Heading</h1>`
),
});
});
test.tags("focus required");
test("add a caption to an image with a link", async () => {
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<a href="https://odoo.com">
<img class="img-fluid test-image" src="${base64Img}">
</a>
<h1>[]Heading</h1>`
),
stepFunction: async (editor) => {
await toggleCaption();
await waitFor("figcaption > input");
const input = queryOne("figure > figcaption > input");
expect(editor.document.activeElement).toBe(input);
// Remove the editor selection for the test because it's irrelevant
// since the focus is not in it.
const selection = editor.document.getSelection();
selection.removeAllRanges();
},
contentAfter: unformat(
`<p><br></p>
<div>
<a href="https://odoo.com">
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption></figcaption>
</figure>
</a>
</div>
<h1>Heading</h1>`
),
});
});
test("add a caption then a link to an image surrounded by text", async () => {
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<p>ab<img class="img-fluid test-image" src="${base64Img}">cd</p>`,
stepFunction: async () => {
await toggleCaption("Hello");
await addLinkToImage("odoo.com");
await expectElementCount(".o-we-linkpopover", 1);
await expectElementCount(".o-we-toolbar", 1);
},
contentAfter: unformat(
`<p>ab</p>
<p>
<a href="https://odoo.com">
<figure>
[<img class="img-fluid test-image" src="${base64Img}">]
<figcaption>Hello</figcaption>
</figure>
</a>
</p>
<p>cd</p>`
),
});
});
test("add a link then a caption to an image surrounded by text", async () => {
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: `<p>ab<img class="img-fluid test-image" src="${base64Img}">cd</p>`,
stepFunction: async (editor) => {
await addLinkToImage("odoo.com");
await animationFrame();
await toggleCaption("Hello");
// Blur the input to commit the caption.
await click("p"); // Blur the input.
await animationFrame(); // Wait for the focus event to trigger a step.
editor.shared.selection.setCursorStart(editor.document.querySelectorAll("p")[1]);
},
contentAfter: unformat(
`<p>ab</p>
<p>[]
<a href="https://odoo.com">
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>Hello</figcaption>
</figure>
</a>
</p>
<p>cd</p>`
),
});
});
test("remove a link from an image with a caption", async () => {
const caption = "Hello";
const captionId = 1;
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<p><br></p>
<a href="https://odoo.com">
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>${caption}</figcaption>
</figure>
</a>
<h1>Heading</h1>`
),
contentBeforeEdit: unformat(
`<p><br></p>
<div class="o-paragraph">
<a href="https://odoo.com">
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption-id="${captionId}" data-caption="${caption}">
<figcaption ${getFigcaptionAttributes(captionId, caption)}>
<input ${CAPTION_INPUT_ATTRIBUTES}>
</figcaption>
</figure>
</a>
</div>
<h1>Heading</h1>`
),
stepFunction: async () => {
await removeLinkFromImage();
await animationFrame();
await expectElementCount(".o-we-linkpopover", 0);
await expectElementCount(".o-we-toolbar", 1);
},
contentAfter: unformat(
`<p><br></p>
<figure>
[<img class="img-fluid test-image" src="${base64Img}">]
<figcaption>Hello</figcaption>
</figure>
<h1>Heading</h1>`
),
});
});
test("remove a caption from an image with a link", async () => {
const caption = "Hello";
const captionId = 1;
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<p><br></p>
<a href="https://odoo.com">
<figure>
<img class="img-fluid test-image" src="${base64Img}">
<figcaption>${caption}</figcaption>
</figure>
</a>
<h1>Heading</h1>`
),
contentBeforeEdit: unformat(
`<p><br></p>
<div class="o-paragraph">
<a href="https://odoo.com">
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption-id="${captionId}" data-caption="${caption}">
<figcaption ${getFigcaptionAttributes(captionId, caption)}>
<input ${CAPTION_INPUT_ATTRIBUTES}>
</figcaption>
</figure>
</a>
</div>
<h1>Heading</h1>`
),
stepFunction: async () => {
await toggleCaption();
await expectElementCount(".o-we-linkpopover", 1);
await expectElementCount(".o-we-toolbar", 1);
},
contentAfter: unformat(
`<p><br></p>
<div>
<a href="https://odoo.com">
[<img class="img-fluid test-image" src="${base64Img}" data-caption="${caption}">]
</a>
</div>
<h1>Heading</h1>`
),
});
});
test("previewing an image with a caption shows the caption as title", async () => {
await setupEditorWithEmbeddedCaption(`<img class="img-fluid test-image" src="${base64Img}">`);
// Preview without a caption shows the file name.
await click("img");
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar button[name='image_preview']");
await animationFrame();
let titleSpan = queryOne(".o-FileViewer .o-FileViewer-header span.text-truncate");
expect(titleSpan.textContent).toBe(base64Img.replaceAll("\n", "%0A"));
await click(".o-FileViewer-headerButton[title='Close (Esc)']");
await animationFrame();
// Add a caption
await toggleCaption("Hello");
await waitForNone(".o-we-toolbar button[name='image_caption']");
// Preview with a caption show the caption.
await click("img");
await waitFor(".o-we-toolbar button[name='image_preview']");
await click(".o-we-toolbar button[name='image_preview']");
await animationFrame();
titleSpan = queryOne(".o-FileViewer .o-FileViewer-header span.text-truncate");
expect(titleSpan.textContent).toBe("Hello");
});
test("previewing an image without caption doesn't show the caption as title (even if data-caption exists)", async () => {
await setupEditorWithEmbeddedCaption(`<img class="img-fluid test-image" src="${base64Img}">`);
// Preview without a caption shows the file name.
await click("img");
await waitFor(".o-we-toolbar button[name='image_preview']");
await click(".o-we-toolbar button[name='image_preview']");
await animationFrame();
let titleSpan = queryOne(".o-FileViewer .o-FileViewer-header span.text-truncate");
expect(titleSpan.textContent).toBe(base64Img.replaceAll("\n", "%0A"));
await click(".o-FileViewer-headerButton[title='Close (Esc)']");
await animationFrame();
// Add a caption
await toggleCaption("Hello");
await waitForNone(".o-we-toolbar button[name='image_caption']");
// Remove the caption
await toggleCaption();
const image = queryOne("img");
expect(image.getAttribute("data-caption")).toBe("Hello");
expect("figure").toHaveCount(0);
// Preview without a caption still shows the file name.
await click("img");
await waitFor(".o-we-toolbar button[name='image_preview']");
await click(".o-we-toolbar button[name='image_preview']");
await animationFrame();
titleSpan = queryOne(".o-FileViewer .o-FileViewer-header span.text-truncate");
expect(titleSpan.textContent).toBe(base64Img.replaceAll("\n", "%0A"));
});
test("should properly parse figure without fig caption", async () => {
await testEditor({
config: configWithEmbeddedCaption,
contentBefore: unformat(
`<figure>
<img class="img-fluid test-image" src="${base64Img}">
</figure>`
),
contentBeforeEdit: unformat(
`<p><br></p>
<figure contenteditable="false">
<img class="img-fluid test-image o_editable_media" src="${base64Img}" data-caption-id="1" data-caption="">
<figcaption data-embedded="caption" data-oe-protected="true" contenteditable="false" class="mt-2" data-embedded-props='{"id":"1","focusInput":false}'>
<input type="text" maxlength="100" class="border-0 p-0" placeholder="Write your caption here">
</figcaption>
</figure>
<p><br></p>
`
),
});
});