292 lines
8.1 KiB
HTML
292 lines
8.1 KiB
HTML
<!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, " ").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>
|