Odoo18-Base/addons/html_editor/static/tests/selection.test.js

255 lines
9.8 KiB
JavaScript
Raw Permalink Normal View History

2025-01-06 10:57:38 +07:00
import { describe, expect, test } from "@odoo/hoot";
import { getActiveElement, press, queryFirst, queryOne } from "@odoo/hoot-dom";
import { animationFrame, tick } from "@odoo/hoot-mock";
import { Component, xml } from "@odoo/owl";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
import { useAutofocus } from "@web/core/utils/hooks";
import { Plugin } from "../src/plugin";
import { MAIN_PLUGINS } from "../src/plugin_sets";
import { setupEditor } from "./_helpers/editor";
import { getContent } from "./_helpers/selection";
import { insertText, tripleClick } from "./_helpers/user_actions";
test("getEditableSelection should work, even if getSelection returns null", async () => {
const { editor } = await setupEditor("<p>a[b]</p>");
let selection = editor.shared.selection.getEditableSelection();
expect(selection.startOffset).toBe(1);
expect(selection.endOffset).toBe(2);
// it happens sometimes in firefox that the selection is null
patchWithCleanup(document, {
getSelection: () => null,
});
selection = editor.shared.selection.getEditableSelection();
expect(selection.startOffset).toBe(1);
expect(selection.endOffset).toBe(2);
});
test("plugins should be notified when ranges are removed", async () => {
let count = 0;
class TestPlugin extends Plugin {
static id = "test";
resources = {
selectionchange_handlers: () => count++,
};
}
const { el } = await setupEditor("<p>a[b]</p>", {
config: { Plugins: [...MAIN_PLUGINS, TestPlugin] },
});
const countBefore = count;
document.getSelection().removeAllRanges();
await animationFrame();
expect(count).toBe(countBefore + 1);
expect(getContent(el)).toBe("<p>ab</p>");
});
test("triple click outside of the Editor", async () => {
const { el } = await setupEditor("<p>[]abc</p><p>d</p>", {});
const anchorNode = el.parentElement;
await tripleClick(el.parentElement);
expect(document.getSelection().anchorNode).toBe(anchorNode);
expect(getContent(el)).toBe("<p>abc</p><p>d</p>");
const p = el.querySelector("p");
await tripleClick(p);
expect(document.getSelection().anchorNode).toBe(p.childNodes[0]);
expect(getContent(el)).toBe("<p>[abc]</p><p>d</p>");
});
test("correct selection after triple click with bold", async () => {
const { el } = await setupEditor("<p>[]abc<strong>d</strong></p><p>efg</p>", {});
await tripleClick(queryFirst("p").firstChild);
expect(getContent(el)).toBe("<p>[abc<strong>d]</strong></p><p>efg</p>");
});
test("fix selection P in the beggining being a direct child of the editable p after selection", async () => {
const { el } = await setupEditor("<div>a</div>[]<p>b</p>");
expect(getContent(el)).toBe(`<div>a</div><p>[]b</p>`);
});
test("fix selection P in the beggining being a direct child of the editable p before selection", async () => {
const { el } = await setupEditor("<p>a</p>[]<div>b</div>");
expect(getContent(el)).toBe(`<p>a[]</p><div>b</div>`);
});
describe("documentSelectionIsInEditable", () => {
test("documentSelectionIsInEditable should be true", async () => {
const { editor } = await setupEditor("<p>a[]b</p>");
const selectionData = editor.shared.selection.getSelectionData();
expect(selectionData.documentSelectionIsInEditable).toBe(true);
});
test("documentSelectionIsInEditable should be false when it is set outside the editable", async () => {
const { editor } = await setupEditor("<p>ab</p>");
const selectionData = editor.shared.selection.getSelectionData();
expect(selectionData.documentSelectionIsInEditable).toBe(false);
});
test("documentSelectionIsInEditable should be false when it is set outside the editable after retrieving it", async () => {
const { editor } = await setupEditor("<p>ab[]</p>");
const selection = document.getSelection();
let selectionData = editor.shared.selection.getSelectionData();
expect(selectionData.documentSelectionIsInEditable).toBe(true);
selection.setPosition(document.body);
// value is updated directly !
selectionData = editor.shared.selection.getSelectionData();
expect(selectionData.documentSelectionIsInEditable).toBe(false);
});
});
test("setEditableSelection should not crash if getSelection returns null", async () => {
const { editor } = await setupEditor("<p>a[b]</p>");
let selection = editor.shared.selection.getEditableSelection();
expect(selection.startOffset).toBe(1);
expect(selection.endOffset).toBe(2);
// it happens sometimes in firefox that the selection is null
patchWithCleanup(document, {
getSelection: () => null,
});
selection = editor.shared.selection.setSelection({
anchorNode: editor.editable.firstChild,
anchorOffset: 0,
});
// Selection could not be set, so it remains unchanged.
expect(selection.startOffset).toBe(1);
expect(selection.endOffset).toBe(2);
});
test("modifySelection should not crash if getSelection returns null", async () => {
const { editor } = await setupEditor("<p>a[b]</p>");
let selection = editor.shared.selection.getEditableSelection();
expect(selection.startOffset).toBe(1);
expect(selection.endOffset).toBe(2);
// it happens sometimes in firefox that the selection is null
patchWithCleanup(document, {
getSelection: () => null,
});
selection = editor.shared.selection.modifySelection("extend", "backward", "word");
// Selection could not be modified.
expect(selection.startOffset).toBe(1);
expect(selection.endOffset).toBe(2);
});
test("setSelection should not set the selection outside the editable", async () => {
const { editor, el } = await setupEditor("<p>a[b]</p>");
editor.document.getSelection().setPosition(document.body);
await tick();
const selection = editor.shared.selection.setSelection(
editor.shared.selection.getEditableSelection()
);
expect(el.contains(selection.anchorNode)).toBe(true);
});
test("press 'ctrl+a' in 'oe_structure' child should only select his content", async () => {
const { el } = await setupEditor(`<div class="oe_structure"><p>a[]b</p><p>cd</p></div>`);
await press(["ctrl", "a"]);
expect(getContent(el)).toBe(`<div class="oe_structure"><p>[ab]</p><p>cd</p></div>`);
});
test("press 'ctrl+a' in 'contenteditable' should only select his content", async () => {
const { el } = await setupEditor(
`<div contenteditable="false"><p contenteditable="true">a[]b</p><p contenteditable="true">cd</p></div>`
);
await press(["ctrl", "a"]);
expect(getContent(el)).toBe(
`<div contenteditable="false"><p contenteditable="true">[ab]</p><p contenteditable="true">cd</p></div>`
);
});
test("restore a selection when you are not in the editable shouldn't move the focus", async () => {
class TestInput extends Component {
static template = xml`<input t-ref="input" t-att-value="'eee'" class="test"/>`;
static props = ["*"];
setup() {
useAutofocus({ refName: "input", mobile: true });
}
}
class TestPlugin extends Plugin {
static id = "test";
static dependencies = ["overlay"];
resources = {
user_commands: [
{
id: "testShowOverlay",
title: "Test",
description: "Test",
run: this.showOverlay.bind(this),
},
],
powerbox_items: [
{
categoryId: "widget",
commandId: "testShowOverlay",
},
],
};
setup() {
this.overlay = this.dependencies.overlay.createOverlay(TestInput);
}
showOverlay() {
this.overlay.open({
props: {},
});
}
}
const { editor } = await setupEditor("<p>te[]st</p>", {
config: { Plugins: [...MAIN_PLUGINS, TestPlugin] },
});
await insertText(editor, "/test");
await press("enter");
await animationFrame();
expect(getActiveElement()).toBe(queryOne("input.test"));
// Something trigger restore
const cursors = editor.shared.selection.preserveSelection();
cursors.restore();
expect(getActiveElement()).toBe(queryOne("input.test"));
});
test("set a collapse selection in a contenteditable false should move it after this node", async () => {
const { el, editor } = await setupEditor(`<p>ab<span contenteditable="false">cd</span>ef</p>`);
editor.shared.selection.setSelection({
anchorNode: queryOne("span[contenteditable='false']"),
anchorOffset: 1,
});
editor.shared.selection.focusEditable();
expect(getContent(el)).toBe(`<p>ab<span contenteditable="false">cd</span>[]ef</p>`);
});
test("preserveSelection's restore should always set the selection, even if it's the same as the current one", async () => {
/**
* There seems to be a bug in Chrome that renders the selection in a
* different position than the one returned by document.getSelection().
* Setting the selection (even if it's the same as the current one) seems to
* solve the issue.
*
* A concrete example:
* <p>abc <a href="#">some link</a> def[]</p>
* press shift + enter
* The selection (in Chrome) is rendered at the following position if
* setBaseAndExtent is skipped when setting the selection after a restore:
* <p>abc <a href="#">some link</a>[] def<br><br></p>
*/
const { editor } = await setupEditor("<p>ab[]cd</p>");
patchWithCleanup(editor.document.getSelection(), {
setBaseAndExtent: () => {
expect.step("setBaseAndExtent");
},
});
const cursors = editor.shared.selection.preserveSelection();
cursors.restore();
expect.verifySteps(["setBaseAndExtent"]);
});