Currently, it sets `Selection` only when it'll partially replaces the text at replacing the text. However, if setting `Selection` before dispatching `beforeinput` event, `beforeinput` event listeners can know the range with `.selectionStart` and `.selectionEnd` even from the text control element. Chrome also does it, so, let's follow the better behavior. Differential Revision: https://phabricator.services.mozilla.com/D260099
406 lines
13 KiB
HTML
406 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<!--
|
|
https://bugzilla.mozilla.org/show_bug.cgi?id=1149826
|
|
-->
|
|
<html>
|
|
<head>
|
|
<title>Test for replaceText</title>
|
|
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
|
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
|
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
|
</head>
|
|
|
|
<body>
|
|
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1149826">Mozilla Bug 1149826</a><br>
|
|
<input type="text"><br>
|
|
<textarea></textarea>
|
|
<div contenteditable></div>
|
|
|
|
<script>
|
|
const gDOMWindowUtils = _getDOMWindowUtils(window);
|
|
const Ci = SpecialPowers.Ci;
|
|
const IS_WIN = navigator.platform.indexOf("Win") == 0;
|
|
|
|
async function testReplaceText(INPUT_TESTS, TEXTAREA_TESTS, CONTENTEDITABLE_TESTS, aPreventSetSelection) {
|
|
await SimpleTest.promiseFocus();
|
|
|
|
const flags = aPreventSetSelection ?
|
|
Ci.nsIDOMWindowUtils.CONTENT_COMMAND_FLAG_PREVENT_SET_SELECTION :
|
|
0;
|
|
|
|
const input = document.querySelector("input");
|
|
input.focus();
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
info("for <input>");
|
|
|
|
for (const TEST of INPUT_TESTS) {
|
|
input.value = TEST.before.value;
|
|
input.selectionStart = TEST.before.start;
|
|
input.selectionEnd = TEST.before.end;
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
input.addEventListener("beforeinput", e => {
|
|
is(e.inputType, "insertReplacementText",
|
|
"inputType in input must be insertReplacementText by replaceText");
|
|
is(input.selectionStart, TEST.replace.start,
|
|
"Before inputReplacementText, start offset should be changed to the replace start");
|
|
is(input.selectionEnd, TEST.replace.start + TEST.replace.src.length,
|
|
"Before inputReplacementText, end offset should be changed to the replace end");
|
|
}, { once: true } );
|
|
|
|
const promiseAfterOnInput =
|
|
new Promise(resolve => input.addEventListener("input", e => {
|
|
is(e.inputType, "insertReplacementText",
|
|
"inputType must be insertReplacementText by replaceText");
|
|
resolve();
|
|
}, { once: true } ));
|
|
gDOMWindowUtils.sendContentCommandEvent(
|
|
"replaceText",
|
|
null,
|
|
TEST.replace.value,
|
|
TEST.replace.start,
|
|
TEST.replace.src,
|
|
flags
|
|
);
|
|
await promiseAfterOnInput
|
|
|
|
is(input.value, TEST.after.value,
|
|
"replaceText in input replaces inner text");
|
|
is(input.selectionStart, TEST.after.start,
|
|
"replaceText in input sets expected selection start");
|
|
is(input.selectionEnd, TEST.after.end,
|
|
"replaceText in input sets expected selection end");
|
|
}
|
|
|
|
const textarea = document.querySelector("textarea");
|
|
textarea.focus();
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
info("for <textarea>");
|
|
|
|
for (const TEST of TEXTAREA_TESTS) {
|
|
textarea.value = TEST.before.value;
|
|
textarea.selectionStart = TEST.before.start;
|
|
textarea.selectionEnd = TEST.before.end;
|
|
|
|
textarea.addEventListener("beforeinput", e => {
|
|
is(e.inputType, "insertReplacementText",
|
|
"inputType must be insertReplacementText by replaceText");
|
|
is(textarea.selectionStart, TEST.replace.start,
|
|
"Before inputReplacementText, start offset be changed to the replace start");
|
|
is(textarea.selectionEnd, TEST.replace.start + TEST.replace.src.length,
|
|
"Before inputReplacementText, end offset be changed to the replace end");
|
|
}, { once: true } );
|
|
|
|
const promiseAfterOnTextarea =
|
|
new Promise(resolve => textarea.addEventListener("input", e => {
|
|
is(e.inputType, "insertReplacementText",
|
|
"inputType must be insertReplacementText by replaceText");
|
|
resolve();
|
|
}, { once: true } ));
|
|
gDOMWindowUtils.sendContentCommandEvent(
|
|
"replaceText",
|
|
null,
|
|
TEST.replace.value,
|
|
TEST.replace.start,
|
|
TEST.replace.src,
|
|
flags
|
|
);
|
|
await promiseAfterOnTextarea
|
|
|
|
is(textarea.value, TEST.after.value,
|
|
"replaceText in textarea replaces inner text");
|
|
is(textarea.selectionStart, TEST.after.start,
|
|
"replaceText in textarea sets expected selection start");
|
|
is(textarea.selectionEnd, TEST.after.end,
|
|
"replaceText in textarea sets expected selection end");
|
|
}
|
|
|
|
const editingHost = document.querySelector("div[contenteditable]");
|
|
editingHost.focus();
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
info("for contenteditable");
|
|
|
|
for (const TEST of CONTENTEDITABLE_TESTS) {
|
|
editingHost.innerHTML = TEST.before.value;
|
|
window.getSelection().setBaseAndExtent(
|
|
// eslint-disable-next-line no-eval
|
|
eval(TEST.before.focusNode),
|
|
TEST.before.focusOffset,
|
|
// eslint-disable-next-line no-eval
|
|
eval(TEST.before.focusNode),
|
|
TEST.before.focusOffset
|
|
);
|
|
|
|
editingHost.addEventListener("beforeinput", e => {
|
|
const selection = window.getSelection();
|
|
is(e.inputType, "insertReplacementText",
|
|
"inputType must be insertReplacementText by replaceText");
|
|
// eslint-disable-next-line no-eval
|
|
is(selection.anchorNode, eval(TEST.replace.textNode),
|
|
"Before inputReplacementText, focus node is the Text containing replacing text");
|
|
is(selection.anchorOffset, TEST.replace.start,
|
|
"Before inputReplacementText, focus offset is start of the replace start");
|
|
// eslint-disable-next-line no-eval
|
|
is(selection.focusNode, eval(TEST.replace.textNode),
|
|
"Before inputReplacementText, focus node is the Text containing replacing text");
|
|
is(selection.focusOffset, TEST.replace.start + TEST.replace.src.length,
|
|
"Before inputReplacementText, focus offset is start of the replace start");
|
|
}, { once: true } );
|
|
|
|
const promiseAfterEditingHost =
|
|
new Promise(resolve => editingHost.addEventListener("input", e => {
|
|
is(e.inputType, "insertReplacementText",
|
|
"inputType must be insertReplacementText by replaceText");
|
|
resolve();
|
|
}, { once: true } ));
|
|
gDOMWindowUtils.sendContentCommandEvent(
|
|
"replaceText",
|
|
null,
|
|
TEST.replace.value,
|
|
TEST.replace.start,
|
|
TEST.replace.src,
|
|
flags
|
|
);
|
|
await promiseAfterEditingHost
|
|
|
|
is(editingHost.textContent, TEST.after.value,
|
|
"replaceText in contenteditable replaces inner text");
|
|
const selection = window.getSelection();
|
|
// eslint-disable-next-line no-eval
|
|
is(selection.focusNode, eval(TEST.after.focusNode),
|
|
"replaceText in contenteditable sets expected focusNode");
|
|
is(selection.focusOffset, TEST.after.focusOffset,
|
|
"replaceText in contenteditable sets expected focusOffset");
|
|
}
|
|
}
|
|
|
|
add_task(async function testReplaceTextWithoutPreventSetSelection() {
|
|
const INPUT_TESTS = [
|
|
{ before: {
|
|
value: "foo", start: 3, end: 3
|
|
},
|
|
replace: {
|
|
src: "o", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbaro", start: 4, end: 4
|
|
}
|
|
},
|
|
{ before: {
|
|
value: "foo ", start: 4, end: 4
|
|
},
|
|
replace: {
|
|
src: "oo", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbar ", start: 4, end: 4
|
|
}
|
|
}];
|
|
|
|
const TEXTAREA_TESTS = [
|
|
{ before: {
|
|
value: "foo", start: 3, end: 3
|
|
},
|
|
replace: {
|
|
src: "o", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbaro", start: 4, end: 4
|
|
}
|
|
},
|
|
{ before: {
|
|
value: "foo ", start: 4, end: 4
|
|
},
|
|
replace: {
|
|
src: "oo", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbar ", start: 4, end: 4
|
|
}
|
|
}];
|
|
|
|
const CONTENTEDITABLE_TESTS = [
|
|
{ before: {
|
|
value: "foo", focusNode: "editingHost.firstChild", focusOffset: 3
|
|
},
|
|
replace: {
|
|
src: "o", value: "bar", start: 1, textNode: "editingHost.firstChild",
|
|
},
|
|
after: {
|
|
value: "fbaro", focusNode: "editingHost.firstChild", focusOffset: 4, isCollapsed: true
|
|
},
|
|
},
|
|
{ before: {
|
|
value: "foo foo", focusNode: "editingHost.firstChild", focusOffset: 4
|
|
},
|
|
replace: {
|
|
src: "oo", value: "bar", start: 1, textNode: "editingHost.firstChild",
|
|
},
|
|
after: {
|
|
value: "fbar foo", focusNode: "editingHost.firstChild", focusOffset: 4, isCollapsed: true
|
|
},
|
|
}];
|
|
|
|
await testReplaceText(INPUT_TESTS, TEXTAREA_TESTS, CONTENTEDITABLE_TESTS, false);
|
|
});
|
|
|
|
add_task(async function testReplaceTextWithPreventSetSelection() {
|
|
const INPUT_TESTS = [
|
|
{ before: {
|
|
value: "foo", start: 3, end: 3
|
|
},
|
|
replace: {
|
|
src: "o", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbaro", start: 5, end: 5
|
|
}
|
|
},
|
|
{ before: {
|
|
value: "foo ", start: 4, end: 4
|
|
},
|
|
replace: {
|
|
src: "oo", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbar ", start: 5, end: 5
|
|
}
|
|
}];
|
|
|
|
const TEXTAREA_TESTS = [
|
|
{ before: {
|
|
value: "foo", start: 3, end: 3
|
|
},
|
|
replace: {
|
|
src: "o", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbaro", start: 5, end: 5
|
|
}
|
|
},
|
|
{ before: {
|
|
value: "foo ", start: 4, end: 4
|
|
},
|
|
replace: {
|
|
src: "oo", value: "bar", start: 1
|
|
},
|
|
after: {
|
|
value: "fbar ", start: 5, end: 5
|
|
}
|
|
}];
|
|
|
|
const CONTENTEDITABLE_TESTS = [
|
|
{ before: {
|
|
value: "foo", focusNode: "editingHost.firstChild", focusOffset: 3
|
|
},
|
|
replace: {
|
|
src: "o", value: "bar", start: 1, textNode: "editingHost.firstChild",
|
|
},
|
|
after: {
|
|
value: "fbaro", focusNode: "editingHost.firstChild", focusOffset: 5, isCollapsed: true
|
|
},
|
|
},
|
|
{ before: {
|
|
value: "foo foo", focusNode: "editingHost.firstChild", focusOffset: 4
|
|
},
|
|
replace: {
|
|
src: "oo", value: "bar", start: 1, textNode: "editingHost.firstChild",
|
|
},
|
|
after: {
|
|
value: "fbar foo", focusNode: "editingHost.firstChild", focusOffset: 5, isCollapsed: true
|
|
},
|
|
}];
|
|
|
|
await testReplaceText(INPUT_TESTS, TEXTAREA_TESTS, CONTENTEDITABLE_TESTS, true);
|
|
});
|
|
|
|
add_task(async function testReplaceTextWithCompositionText() {
|
|
await SimpleTest.promiseFocus();
|
|
|
|
// Don't replace text during composition
|
|
const input = document.querySelector("input");
|
|
input.value = "";
|
|
input.focus();
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
let promise =
|
|
new Promise(resolve => input.addEventListener("compositionupdate", resolve, { once: true }));
|
|
synthesizeCompositionChange(
|
|
{ "composition":
|
|
{ "string": "foo",
|
|
"clauses":
|
|
[
|
|
{ "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
|
|
]
|
|
},
|
|
});
|
|
await promise;
|
|
|
|
input.addEventListener("input", e => {
|
|
isnot(e.inputType, "insertReplacementText",
|
|
"Don't fire insertReplacementText input event by replaceText");
|
|
}, { once: true } );
|
|
gDOMWindowUtils.sendContentCommandEvent("replaceText", null, "bar", 1, "o");
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
promise = new Promise(resolve => input.addEventListener("compositionend", resolve, { once: true }));
|
|
synthesizeComposition({type: "compositioncommitasis", key: {key: "KEY_Enter"}});
|
|
await promise;
|
|
|
|
is(input.value, "foo",
|
|
"replaceText doesn't replace inner text when having composition");
|
|
is(input.selectionStart, 3, "replaceText sets caret position to next of replaced text");
|
|
is(input.selectionStart, input.selectionEnd, "replaceText sets that selection is collapsed");
|
|
});
|
|
|
|
add_task(async function testReplaceTextBeforeCallingPreventDefault() {
|
|
await SimpleTest.promiseFocus();
|
|
|
|
// Call preventDefault on beforeinput
|
|
const input = document.querySelector("input");
|
|
input.value = "foo";
|
|
input.focus();
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
input.selectionStart = 1
|
|
input.selectionEnd = 2;
|
|
|
|
const promise = new Promise(resolve => input.addEventListener("beforeinput", e => {
|
|
e.preventDefault();
|
|
resolve();
|
|
}, { once: true }));
|
|
gDOMWindowUtils.sendContentCommandEvent("replaceText", null, "bar", 1, "o");
|
|
await promise;
|
|
|
|
is(input.value, "foo",
|
|
"replaceText doesn't replace inner text of <input> since preventDefault is called");
|
|
is(input.selectionStart, 1, "selectionStart isn't changed since preventDefault is called");
|
|
is(input.selectionEnd, 2, "selectionEnd isn't changed since preventDefault is called");
|
|
});
|
|
|
|
add_task(async function testReplaceTextWithoutMatch() {
|
|
await SimpleTest.promiseFocus();
|
|
|
|
const input = document.querySelector("input");
|
|
input.value = "foo";
|
|
input.focus();
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
input.selectionStart = 1
|
|
input.selectionEnd = 2;
|
|
|
|
gDOMWindowUtils.sendContentCommandEvent("replaceText", null, "bar", 1, "a");
|
|
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
|
|
|
is(input.value, "foo",
|
|
"replaceText doesn't replace inner text of <input> due to not matched");
|
|
is(input.selectionStart, 1, "selectionStart isn't changed due to failed");
|
|
is(input.selectionEnd, 2, "selectionEnd isn't changed due to failed");
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|