Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE html>
<title>Test Windows conventional caret movement behavior</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
<style>
[contenteditable] {
font-size: 14px;
font-family: monospace;
overflow-wrap: normal;
outline: none;
width: 35ch;
}
textarea {
font-size: 14px;
font-family: monospace;
}
.break-word {
overflow-wrap: break-word;
}
</style>
<div id="testDiv" contenteditable></div>
<textarea id="testTextarea" cols=35></textarea>
<script>
// Call testEach(cases[i]) on console when debugging
/**
* @type {TestCase[]}
*
* @typedef {object} TestCase
* @property {string} title
* @property {string} text Text on which the test runs.
* The newlines and spaces will be auto-converted if needed.
* @property {boolean} [reverse] Test in both forward and backward directions
* @property {boolean} [backward] Move backward by cmd_wordPrevious
* @property {ElementSpecific} [div] Can be omitted when there is a bug
* @property {ElementSpecific} textarea
*
* @typedef {object} ElementSpecific
* @property {number} expectedOffset Expected offset after a caret move
* @property {number} [expectedNodeIndex] The index of child node with focus after a caret move.
* -1 means the parent node.
* @property {number} [initialOffset=0] initialOffset offset before a caret move
* @property {number} [initialNodeIndex]
*/
const kFirstWordLength = "Supercalifragilisticexpialidocious".length; // 34
const kParentNodeIndex = -1; // special value
const cases = [
{
title: "Eats inline whitespaces after word",
text: "Supercalifragilisticexpialidocious foo bar fussball",
reverse: true,
div: {
expectedOffset: kFirstWordLength + 1
},
textarea: {
expectedOffset: kFirstWordLength + 1
}
},
{
title: "Eats inline whitespaces across a wrapped line after word",
text: "Supercalifragilisticexpialidocious foo bar fussball",
reverse: true,
div: {
expectedOffset: kFirstWordLength + 7
},
textarea: {
expectedOffset: kFirstWordLength + 7
}
},
{
title: "Eats inline whitespaces across multiple wrapped lines after word",
text: `Supercalifragilisticexpialidocious foo bar fussball`,
reverse: true,
div: {
expectedOffset: kFirstWordLength + 65
},
textarea: {
expectedOffset: kFirstWordLength + 65
}
},
{
title: "Eats inline whitespaces after a whole word and stop before a newline",
text: "Supercalifragilisticexpialidocious \nfoo bar fussball",
reverse: true,
div: {
expectedOffset: 1,
expectedNodeIndex: kParentNodeIndex
},
textarea: {
expectedOffset: kFirstWordLength + 1
}
},
{
title: "Eats inline whitespaces after a partial word and stop before a newline",
text: "Supercalifragilisticexpialidocious \nfoo bar fussball",
div: {
initialOffset: 5,
expectedOffset: 1,
expectedNodeIndex: kParentNodeIndex
},
textarea: {
initialOffset: 5,
expectedOffset: kFirstWordLength + 1
}
},
{
title: "Eats inline whitespaces without a word and stop before a newline",
text: "Supercalifragilisticexpialidocious \n foo bar fussball",
// TODO(krosylight): Currently ClusterIterator internally skips trailing whitespace
// div: {
// initialOffset: kFirstWordLength,
// expectedOffset: 1,
// expectedNodeIndex: kParentNodeIndex
// },
textarea: {
initialOffset: kFirstWordLength,
expectedOffset: kFirstWordLength + 1
}
},
{
title: "Jumps to the next line and eats inline whitespaces",
text: "Supercalifragilisticexpialidocious \n foo bar fussball",
reverse: true,
div: {
initialOffset: kFirstWordLength + 1,
expectedOffset: 6,
expectedNodeIndex: 2
},
textarea: {
initialOffset: kFirstWordLength + 1,
expectedOffset: kFirstWordLength + 8
}
},
{
title: "Stops on whitespaces after word",
text: "Supercalifragilisticexpialidocious \n foo bar fussball",
backward: true,
div: {
initialOffset: 8,
initialNodeIndex: 2,
expectedOffset: 6,
expectedNodeIndex: 2
},
textarea: {
initialOffset: 44, // middle of "foo"
expectedOffset: 42
}
},
// TODO(krosylight): Currently no way to tell a word break is from line wrapping
// {
// title: "Ignore a word break introduced by line wrapping",
// text: "Supercalifragilisticexpialidociouspostfix",
// className: "narrow break-word",
// div: {
// expectedOffset: kFirstWordLength + 7
// },
// textarea: {
// expectedOffset: kFirstWordLength + 7
// }
// }
{
title: "Jump only one line at an empty line end",
text: "Supercalifragilisticexpialidocious \n\nfoo bar fussball",
reverse: true,
div: {
initialOffset: 2,
initialNodeIndex: kParentNodeIndex,
expectedOffset: 0,
expectedNodeIndex: 3
},
textarea: {
initialOffset: kFirstWordLength + 2,
expectedOffset: kFirstWordLength + 3
}
},
{
title: "Jump only one line at a non-empty line end",
text: "Supercalifragilisticexpialidocious \n\nfoo bar fussball",
reverse: true,
div: {
initialOffset: kFirstWordLength + 1,
expectedOffset: 2,
expectedNodeIndex: -1
},
textarea: {
initialOffset: kFirstWordLength + 1,
expectedOffset: kFirstWordLength + 2
}
},
];
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(async () => {
await SpecialPowers.pushPrefEnv({
set: [["layout.word_select.eat_space_to_next_word", true]]
});
// Test on div first to prevent Bug 1623413
for (const testCase of cases) {
if (testCase.div) {
testOnDiv(testCase);
}
}
for (const testCase of cases) {
testOnTextarea(testCase);
}
SimpleTest.finish();
});
/** @param {TestCase} testCase */
function testOnDiv(testCase) {
const {
title,
backward = false,
reverse = false,
} = testCase;
const {
initialOffset = 0,
expectedOffset,
} = testCase.div;
if (expectedOffset === undefined) {
throw new Error("`expectedOffset` must be defined.")
}
testDiv.innerHTML = testCase.text.replaceAll(/ {2}/g, " &nbsp;").replaceAll(/\n/g, "<br>");
const initialNode = childNode(testDiv, testCase.div.initialNodeIndex);
const expectedNode = childNode(testDiv, testCase.div.expectedNodeIndex);
const selection = getSelection();
selection.collapse(initialNode, initialOffset);
moveByWord(backward);
is(selection.focusNode, expectedNode, `focusNode in "${title}"`);
is(selection.focusOffset, expectedOffset, `focusOffset in "${title}"`);
if (reverse) {
selection.collapse(expectedNode, expectedOffset);
moveByWord(!backward);
is(selection.focusNode, initialNode, `focusNode with reversed selection in "${title}"`);
is(selection.focusOffset, initialOffset, `focusOffset with reversed selection in "${title}"`);
}
}
function testOnTextarea(testCase) {
const {
title,
backward = false,
reverse = false,
} = testCase;
const {
initialOffset = 0,
expectedOffset,
} = testCase.textarea;
if (expectedOffset === undefined) {
throw new Error("`expectedOffset` must be defined.")
}
testTextarea.value = testCase.text;
testTextarea.selectionStart = testTextarea.selectionEnd = initialOffset;
testTextarea.focus();
moveByWord(backward);
is(testTextarea.selectionStart, expectedOffset, `selectionStart in "${title}"`);
if (reverse) {
testTextarea.selectionStart = testTextarea.selectionEnd = expectedOffset;
moveByWord(!backward);
is(testTextarea.selectionStart, initialOffset, `selectionStart with reversed selection in "${title}"`);
}
}
function childNode(parent, index = 0) {
if (index === kParentNodeIndex) {
return parent;
}
return parent.childNodes[index];
}
/** @param {boolean} backward */
function moveByWord(backward) {
const dir = backward ? "Previous" : "Next";
SpecialPowers.doCommand(window, `cmd_word${dir}`);
}
</script>