layout/generic/test/test_bug1623764.html
author Stanca Serban <sstanca@mozilla.com>
Fri, 25 Nov 2022 18:35:18 +0200
changeset 643645 8b092cca2cab001ed8d13fc83d17bdba39cffe0d
parent 544046 00911e3c92c310a0344fb7da762727049240b1ba
permissions -rw-r--r--
Backed out changeset dab070a6ba77 (bug 1802125) for causing wpt failures on /clipboard-apis. CLOSED TREE

<!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>