Files
firefox/editor/libeditor/tests/test_nsIEditor_insertNode.html
Masayuki Nakano bf81531680 Bug 1764895 - part 1: Make nsIEditor.insertNode and nsIEditor.deleteNode take an optional parameter to preserve selection r=m_kato
`nsIEditor.setShouldTxnSetSelection` can preserve selection across multiple
editing.  Therefore, we cannot manage the state only in the stack.

It's used only in comm-central, and used only with `insertNode` and
`deleteNode`.  Therefore, adding new param to them to preserve selection
must be enough.

While I'm writing this patch, I realized that `input` event is not fired
by these methods because nobody set a placeholder transaction.  That may
lead Thunderbird only IME crash bugs due to `IMEContentObserver` is not
notified editor properly.  Therefore, this may fix some Thunderbird only
crashes.

Note that `deleteNode` should not update selection.  However, I'm not 100%
sure that.  Therefore, I add new param to `deleteNode` too.  However,
some reviewers think it's unnecessary, I'll remove it before landing.

Finally, `beforeinput` and `input` caused by the method calls start updating
selection.  However, I think that it should be better behavior.  If Thunderbird
needs to guarantee that selection is set to whether the user expected when
it calls these methods with preserving selection.

Differential Revision: https://phabricator.services.mozilla.com/D196004
2023-12-13 07:50:42 +00:00

215 lines
6.7 KiB
HTML

<!doctype>
<html>
<head>
<meta charset="utf-8">
<title>nsIEditor.insertNode</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
<script>
"use strict";
function stringifyInputEvent(aEvent) {
if (!aEvent) {
return "null";
}
return `${aEvent.type}: { inputType=${aEvent.inputType} }`;
}
function getRangeDescription(range) {
function getNodeDescription(node) {
if (!node) {
return "null";
}
switch (node.nodeType) {
case Node.TEXT_NODE:
return `${node.nodeName} "${node.data}"`;
case Node.ELEMENT_NODE:
return `<${node.nodeName.toLowerCase()}>`;
default:
return `${node.nodeName}`;
}
}
if (range === null) {
return "null";
}
if (range === undefined) {
return "undefined";
}
return range.startContainer == range.endContainer &&
range.startOffset == range.endOffset
? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
: `(${getNodeDescription(range.startContainer)}, ${
range.startOffset
}) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
}
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(() => {
const editingHost = document.querySelector("div[contenteditable]");
const editor =
SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
editingHost.focus();
let events = [];
editingHost.addEventListener("input", event => events.push(event));
(function test_insert_text_to_start() {
editor.insertNode(document.createTextNode("abc"), editingHost, 0);
is(
editingHost.innerHTML,
"abc<br>",
"test_insert_text_to_start: insertNode() should insert new text node at start of the container"
);
is(
events.length,
1,
"test_insert_text_to_start: Only one input event should be fired when insertNode() inserts a text node"
);
is(
stringifyInputEvent(events[0]),
stringifyInputEvent({ type: "input", inputType: "" }),
"test_insert_text_to_start: input event should be fired when inserting a node"
);
is(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: editingHost,
startOffset: 1,
endContainer: editingHost,
endOffset: 1,
}),
"test_insert_text_to_start: insertNode() should collapse selection after the inserted text node"
);
})();
(function test_insert_span_to_big_index() {
events = [];
editingHost.innerHTML = "abc";
const span = document.createElement("span");
span.textContent = "def";
editor.insertNode(span, editingHost, 1000);
is(
editingHost.innerHTML,
"abc<span>def</span>",
"test_insert_span_to_big_index: insertNode() with big index should insert new node at end of the container"
);
is(
events.length,
1,
"test_insert_span_to_big_index: Only one input event should be fired when insertNode() inserts a node"
);
is(
stringifyInputEvent(events[0]),
stringifyInputEvent({ type: "input", inputType: "" }),
"test_insert_span_to_big_index: input event should be fired when inserting a node"
);
is(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: editingHost,
startOffset: 2,
endContainer: editingHost,
endOffset: 2,
}),
"test_insert_span_to_big_index: insertNode() should collapse selection after the inserted node"
);
})();
(function test_preserve_selection() {
events = [];
editingHost.innerHTML = "abc";
const span = document.createElement("span");
span.textContent = "def";
getSelection().collapse(editingHost, 0);
editor.insertNode(span, editingHost, 1, true);
is(
editingHost.innerHTML,
"abc<span>def</span>",
"test_preserve_selection: insertNode() should insert new node at end of the container"
);
is(
events.length,
1,
"test_preserve_selection: Only one input event should be fired when insertNode() inserts a node"
);
is(
stringifyInputEvent(events[0]),
stringifyInputEvent({ type: "input", inputType: "" }),
"test_preserve_selection: input event should be fired when inserting a node"
);
is(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: editingHost,
startOffset: 0,
endContainer: editingHost,
endOffset: 0,
}),
"test_preserve_selection: insertNode() should not collapse selection after the inserted node"
);
})();
(function test_not_preserve_selection_nested_by_beforeinput() {
editingHost.innerHTML = "abc";
const span1 = document.createElement("span");
span1.textContent = "def";
const span2 = document.createElement("span");
span2.textContent = "ghi";
getSelection().collapse(editingHost, 0);
editingHost.addEventListener("beforeinput", () => {
editor.insertNode(span1, editingHost, 1);
}, {once: true});
editor.insertNode(span2, editingHost, 2, true);
is(
editingHost.innerHTML,
"abc<span>def</span><span>ghi</span>",
"test_not_preserve_selection_nested_by_beforeinput: both insertNode() should work"
);
is(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: editingHost,
startOffset: 2,
endContainer: editingHost,
endOffset: 2,
}),
"test_not_preserve_selection_nested_by_beforeinput: only insertNode() called in beforeinput listener should update selection"
);
})();
(function test_not_preserve_selection_nested_by_input() {
editingHost.innerHTML = "abc";
const span1 = document.createElement("span");
span1.textContent = "def";
const span2 = document.createElement("span");
span2.textContent = "ghi";
getSelection().collapse(editingHost, 0);
editingHost.addEventListener("input", () => {
editor.insertNode(span2, editingHost, 2);
}, {once: true});
editor.insertNode(span1, editingHost, 1, true);
is(
editingHost.innerHTML,
"abc<span>def</span><span>ghi</span>",
"test_not_preserve_selection_nested_by_input: both insertNode() should work"
);
is(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: editingHost,
startOffset: 3,
endContainer: editingHost,
endOffset: 3,
}),
"test_not_preserve_selection_nested_by_input: only insertNode() called in input listener should update selection"
);
})();
SimpleTest.finish();
});
</script>
</head>
<body><div contenteditable><br></div></body>
</html>