<!doctype html>
<meta charset=utf-8>
<title>Test that execCommand with &lt;input&gt; or &lt;textarea&gt;</title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<div id="container"></div>
<script>
"use strict";

setup({explicit_done: true});

/**
 * This test checks whether document.execCommand() does something expected or
 * not in <input> and <textarea> with/without contenteditable parent.  Although
 * this is not standardized even by any drafts.  So, this test uses expected
 * values which may be expected by web developers.
 */
function runTests() {
  let container = document.getElementById("container");
  container.innerHTML = "Here <b>is</b> Text: <input id=\"target\">";
  runTest(document.getElementById("target"), "In <input>");
  container.innerHTML = "Here <b>is</b> Text: <textarea id=\"target\"></textarea>";
  runTest(document.getElementById("target"), "In <textarea>");
  container.setAttribute("contenteditable", "true");
  container.innerHTML = "Here <b>is</b> Text: <input id=\"target\">";
  runTest(document.getElementById("target"), "In <input> in contenteditable");
  container.innerHTML = "Here <b>is</b> Text: <textarea id=\"target\"></textarea>";
  runTest(document.getElementById("target"), "In <textarea> in contenteditable");

  done();
}

function runTest(aTarget, aDescription) {
  const kIsTextArea = aTarget.tagName === "TEXTAREA";
  const kTests = [
    /**
     * command: command name of execCommand().
     * param: param for the command.  i.e., the 3rd param of execCommand().
     * value: initial value of <input> or <textarea>.  must have a pair of
     *        "[" and "]" for specifying selection range.
     * expectedValue: expected value of <input> or <textarea> after calling
     *                execCommand() with command and param.  must have a
     *                pair of "[" and "]" for specifying selection range.
     * expectedExecCommandResult: expected bool result of execCommand().
     * expectedCommandSupported: expected bool result of queryCommandSupported().
     * expectedCommandEnabled: expected bool result of queryCommandEnabled().
     * beforeinputExpected: if "beforeinput" event shouldn't be fired, set
     *                      null.  otherwise, expected inputType value and
     *                      target element.
     * inputExpected: if "input" event shouldn't be fired, set null.
     *                otherwise, expected inputType value and target element.
     */
    {command: "getHTML", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: false,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "bold", param: "bold",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "italic", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "underline", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "strikethrough", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "superscript", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    // Should return true for web apps implementing custom editor.
    {command: "cut", param: null,
     value: "ab[]c", expectedValue: "ab[]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "cut", param: null,
     value: "a[b]c", expectedValue: "a[]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "deleteByCut", target: aTarget },
    },
    // Should return true for web apps implementing custom editor.
    {command: "copy", param: null,
     value: "abc[]d", expectedValue: "abc[]d",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "copy", param: null,
     value: "a[bc]d", expectedValue: "a[bc]d",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "paste", param: null,
     value: "a[]c", expectedValue: "a[bc]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "insertFromPaste", target: aTarget },
    },
    {command: "delete", param: null,
     value: "ab[]c", expectedValue: "a[]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "deleteContentBackward", target: aTarget },
    },
    {command: "delete", param: null,
     value: "a[b]c", expectedValue: "a[]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "deleteContentBackward", target: aTarget },
    },
    {command: "forwarddelete", param: null,
     value: "a[b]c", expectedValue: "a[]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "deleteContentForward", target: aTarget },
    },
    {command: "forwarddelete", param: null,
     value: "a[]bc", expectedValue: "a[]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "deleteContentForward", target: aTarget },
    },
    {command: "selectall", param: null,
     value: "a[b]c", expectedValue: "[abc]",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null, inputExpected: null,
    },
    // Setting value should forget any transactions.
    {command: "undo", param: null,
     value: "[a]bc", expectedValue: "[a]bc",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "undo", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     initFunc: () => {
       document.execCommand("delete", false, null);
     },
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "historyUndo", target: aTarget },
    },
    // Setting value should forget any transactions.
    {command: "redo", param: null,
     value: "[a]bc", expectedValue: "[a]bc",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "redo", param: null,
     value: "a[b]c", expectedValue: "a[]c",
     initFunc: () => {
       document.execCommand("delete", false, null);
       document.execCommand("undo", false, null);
     },
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "historyRedo", target: aTarget },
    },
    {command: "indent", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "outdent", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "backcolor", param: "#000000",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "forecolor", param: "#000000",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "hilitecolor", param: "#000000",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "fontname", param: "DummyFont",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "fontsize", param: "5",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "increasefontsize", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: false,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "decreasefontsize", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: false,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "inserthorizontalrule", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "createlink", param: "foo.html",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "insertimage", param: "no-image.png",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "inserthtml", param: "<b>inserted</b>",
     value: "a[b]c", expectedValue: "ainserted[]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "insertText", target: aTarget },
    },
    {command: "inserttext", param: "**inserted**",
     value: "a[b]c", expectedValue: "a**inserted**[]c",
     expectedExecCommandResult: true,
     expectedCommandSupported: true,
     expectedCommandEnabled: true,
     beforeinputExpected: null,
     inputExpected: { inputType: "insertText", target: aTarget },
    },
    {command: "justifyleft", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "justifyright", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "justifycenter", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "justifyfull", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "removeformat", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "unlink", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "insertorderedlist", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "insertunorderedlist", param: null,
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "insertparagraph", param: null,
     value: "a[b]c", expectedValue: kIsTextArea ? "a\n[]c" : "a[b]c",
     expectedExecCommandResult: kIsTextArea,
     expectedCommandSupported: true,
     expectedCommandEnabled: kIsTextArea,
     beforeinputExpected: null,
     inputExpected: kIsTextArea ? { inputType: "insertParagraph", target: aTarget } : null,
    },
    {command: "insertlinebreak", param: null,
     value: "a[b]c", expectedValue: kIsTextArea ? "a\n[]c" : "a[b]c",
     expectedExecCommandResult: kIsTextArea,
     expectedCommandSupported: true,
     expectedCommandEnabled: kIsTextArea,
     beforeinputExpected: null,
     inputExpected: kIsTextArea ? { inputType: "insertLineBreak", target: aTarget } : null,
    },
    {command: "formatblock", param: "div",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: true,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "heading", param: "h1",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: false,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
    },
    {command: "styleWithCSS", param: "true",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: container.isContentEditable,
     expectedCommandSupported: true,
     expectedCommandEnabled: container.isContentEditable,
     beforeinputExpected: null, inputExpected: null,
     additionalCheckFunc: aDescription => {
       test(
         () => assert_equals(document.queryCommandState("styleWithCSS"), container.isContentEditable),
         `${aDescription}: styleWithCSS state should be ${container.isContentEditable} when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } has focus`
       );
       aTarget.blur();
       container.focus();
       getSelection().collapse(container, 0);
       test(
         () => assert_equals(document.queryCommandState("styleWithCSS"), container.isContentEditable),
         `${aDescription}: styleWithCSS state should be ${container.isContentEditable} when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } does not have focus`
        );
      },
    },
    {command: "styleWithCSS", param: "false",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: container.isContentEditable,
     expectedCommandSupported: true,
     expectedCommandEnabled: container.isContentEditable,
     beforeinputExpected: null, inputExpected: null,
     additionalCheckFunc: aDescription => {
       test(
         () => assert_equals(document.queryCommandState("styleWithCSS"), false),
         `${aDescription}: styleWithCSS state should be false when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } has focus`
       );
       aTarget.blur();
       container.focus();
       getSelection().collapse(container, 0);
       test(
         () => assert_equals(document.queryCommandState("styleWithCSS"), false),
         `${aDescription}: styleWithCSS state should be false when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } does not have focus`
        );
      },
    },
    {command: "contentReadOnly", param: "true",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: false,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
     additionalCheckFunc: aDescription => {
       test(
         () => assert_equals(document.queryCommandState("contentReadOnly"), false),
         `${aDescription}: contentReadOnly state should be true when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } has focus`
       );
       test(
         () => assert_equals(aTarget.readOnly, false),
         `${aDescription}: readonly property should be true`
       );
       aTarget.blur();
       container.focus();
       getSelection().collapse(container, 0);
       test(
         () => assert_equals(document.queryCommandState("contentReadOnly"), false),
         `${aDescription}: contentReadOnly state should be false when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } does not have focus`
        );
      },
    },
    {command: "contentReadOnly", param: "false",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: false,
     expectedCommandSupported: false,
     expectedCommandEnabled: false,
     beforeinputExpected: null, inputExpected: null,
     additionalCheckFunc: aDescription => {
       test(
         () => assert_equals(document.queryCommandState("contentReadOnly"), false),
         `${aDescription}: contentReadOnly state should be false when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } has focus`
       );
       test(
         () => assert_equals(aTarget.readOnly, false),
         `${aDescription}: readonly property should be false`
       );
       aTarget.blur();
       container.focus();
       getSelection().collapse(container, 0);
       test(
         () => assert_equals(document.queryCommandState("contentReadOnly"), false),
         `${aDescription}: contentReadOnly state should be false when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } does not have focus`
        );
      },
    },
    {command: "defaultParagraphSeparator", param: "p",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: container.isContentEditable,
     expectedCommandSupported: true,
     expectedCommandEnabled: container.isContentEditable,
     beforeinputExpected: null, inputExpected: null,
     additionalCheckFunc: aDescription => {
       test(
         () =>
           assert_equals(
             document.queryCommandValue("defaultParagraphSeparator"),
             container.isContentEditable ? "p" : "div"
           )
         ,
         `${aDescription}: defaultParagraphSeparator value should be "p" when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } has focus`
       );
       aTarget.blur();
       container.focus();
       getSelection().collapse(container, 0);
       test(
         () =>
           assert_equals(
             document.queryCommandValue("defaultParagraphSeparator"),
             container.isContentEditable ? "p" : "div"
           ),
         `${aDescription}: defaultParagraphSeparator value should be "p" when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } does not have focus`
        );
      },
    },
    {command: "defaultParagraphSeparator", param: "div",
     value: "a[b]c", expectedValue: "a[b]c",
     expectedExecCommandResult: container.isContentEditable,
     expectedCommandSupported: true,
     expectedCommandEnabled: container.isContentEditable,
     beforeinputExpected: null, inputExpected: null,
     additionalCheckFunc: aDescription => {
       test(
         () => assert_equals(document.queryCommandValue("defaultParagraphSeparator"), "div"),
         `${aDescription}: defaultParagraphSeparator value should be "div" when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } has focus`
       );
       aTarget.blur();
       container.focus();
       getSelection().collapse(container, 0);
       test(
         () => assert_equals(document.queryCommandValue("defaultParagraphSeparator"), "div"),
         `${aDescription}: defaultParagraphSeparator value should be "div" when ${
           kIsTextArea ? "<textarea>" : "<input>"
         } does not have focus`
        );
      },
    },
  ];

  for (const kTest of kTests) {
    const kDescription =
        `${aDescription}, execCommand("${kTest.command}", false, ${kTest.param}), ${kTest.value})`;
    aTarget.value = "dummy value to ensure the following value setting clear the undo history";
    let value = kTest.value.replace(/[\[\]]/g, "");
    aTarget.value = value;
    aTarget.focus();
    aTarget.selectionStart = kTest.value.indexOf("[");
    aTarget.selectionEnd = kTest.value.indexOf("]") - 1;

    test(
      () => assert_equals(document.queryCommandSupported(kTest.command), kTest.expectedCommandSupported),
      `${kDescription}: The command should ${
        kTest.expectedCommandSupported ? "be" : "not be"
      } supported`
    );
    test(
      () => assert_equals(document.queryCommandEnabled(kTest.command), kTest.expectedCommandEnabled),
      `${kDescription}: The command should ${
        kTest.expectedCommandEnabled ? "be" : "not be"
      } enabled`
    );

    if (!document.queryCommandSupported(kTest.command) || !kTest.expectedCommandSupported) {
      continue;
    }

    if (kTest.initFunc) {
      kTest.initFunc();
    }

    let beforeinput = null;
    function onBeforeinput(event) {
      beforeinput = event;
    }
    window.addEventListener("beforeinput", onBeforeinput, {capture: true});
    let input = null;
    function onInput(event) {
      input = event;
    }
    window.addEventListener("input", onInput, {capture: true});
    let ret;
    test(function () {
      ret = document.execCommand(kTest.command, false, kTest.param);
      assert_equals(ret, kTest.expectedExecCommandResult);
    }, `${kDescription}: execCommand() should return ${kTest.expectedExecCommandResult}`);
    test(function () {
      let value = aTarget.value.substring(0, aTarget.selectionStart) +
                      "[" +
                      aTarget.value.substring(aTarget.selectionStart, aTarget.selectionEnd) +
                      "]" +
                      aTarget.value.substring(aTarget.selectionEnd);
      assert_equals(value, kTest.expectedValue);
    }, `${kDescription}: ${kIsTextArea ? "<textarea>" : "<input>"}.value should be "${kTest.expectedValue}"`);
    test(function () {
      assert_equals(beforeinput?.inputType, kTest.beforeinputExpected?.inputType);
    }, `${kDescription}: beforeinput.inputType should be ${kTest.beforeinputExpected?.inputType}`);
    test(function () {
      assert_equals(beforeinput?.target, kTest.beforeinputExpected?.target);
    }, `${kDescription}: beforeinput.target should be ${kTest.beforeinputExpected?.target}`);
    test(function () {
      assert_equals(input?.inputType, kTest.inputExpected?.inputType);
    }, `${kDescription}: input.inputType should be ${kTest.inputExpected?.inputType}`);
    test(function () {
      assert_equals(input?.target, kTest.inputExpected?.target);
    }, `${kDescription}: input.target should be ${kTest.inputExpected?.target}`);
    if (kTest.additionalCheckFunc) {
      kTest.additionalCheckFunc(kDescription);
    }
    window.removeEventListener("beforeinput", onBeforeinput, {capture: true});
    window.removeEventListener("input", onInput, {capture: true});
  }
}

window.addEventListener("load", runTests, {once: true});
</script>
