chromium/third_party/blink/web_tests/external/wpt/editing/run/undo-redo.html

<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="../include/editor-test-utils.js"></script>
<iframe srcdoc=""></iframe>
<script>
"use strict";
const iframe = document.querySelector("iframe");

promise_test(async () => {
  await new Promise(resolve => {
    addEventListener("load", resolve, {once: true});
  });
}, "Waiting for load...");

/**
 * This test does NOT test whether the edit result is valid or invalid.
 * This test just tests whether "undo" and "redo" restores previous state
 * and additional "undo" and "redo" does not run unexpectedly.
 *
 * description: Set string to explain what's testing.
 * editorInnerHTML: Set initial innerHTML value of editor.
 * init: Set a function object if you need to test complicated cases, e.g.,
 *       testing with empty text node.
 * run: Set a function object which run something modifying the editor (or
 *      does nothing).
 * expectedUndoResult: Set an expected innerHTML result as string or array
 *                     of the string.  If this is not specified, it's compared
 *                     with editorInnerHTML value.
 * cleanUp: Set a function object if you need to clean something up after the
 *          test.
 */

const tests = [
  {
    description: "insertParagraph at start of a paragraph",
    editorInnerHTML: "<p>[]abcdef</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertParagraph");
    },
  },
  {
    description: "insertParagraph at middle of a paragraph",
    editorInnerHTML: "<p>abc[]def</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertParagraph");
    },
  },
  {
    description: "insertParagraph at end of a paragraph",
    editorInnerHTML: "<p>abcdef[]</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertParagraph");
    },
  },
  {
    description: "insertParagraph at start of a listitem",
    editorInnerHTML: "<ul><li>[]abcdef</li></ul>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertParagraph");
    },
  },
  {
    description: "insertParagraph at middle of a listitem",
    editorInnerHTML: "<ul><li>abc[]def</li></ul>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertParagraph");
    },
  },
  {
    description: "insertParagraph at end of a listitem",
    editorInnerHTML: "<ul><li>abcdef[]</li></ul>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertParagraph");
    },
  },
  {
    description: "insertLineBreak at start of a paragraph",
    editorInnerHTML: "<p>[]abcdef</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertLineBreak");
    },
  },
  {
    description: "insertLineBreak at middle of a paragraph",
    editorInnerHTML: "<p>abc[]def</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertLineBreak");
    },
  },
  {
    description: "insertLineBreak at end of a paragraph",
    editorInnerHTML: "<p>abcdef[]</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertLineBreak");
    },
  },
  {
    description: "insertLineBreak at start of a listitem",
    editorInnerHTML: "<ul><li>[]abcdef</li></ul>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertLineBreak");
    },
  },
  {
    description: "insertLineBreak at middle of a listitem",
    editorInnerHTML: "<ul><li>abc[]def</li></ul>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertLineBreak");
    },
  },
  {
    description: "insertLineBreak at end of a listitem",
    editorInnerHTML: "<ul><li>abcdef[]</li></ul>",
    run: (win, doc, editingHost) => {
      doc.execCommand("insertLineBreak");
    },
  },
  {
    description: "delete at start of second paragraph",
    editorInnerHTML: "<p>abc</p><p>[]def</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("delete");
    }
  },
  {
    description: "forwarddelete at end of first paragraph",
    editorInnerHTML: "<p>abc[]</p><p>def</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("forwarddelete");
    }
  },
  {
    description: "delete at start of second paragraph starting with an emoji",
    editorInnerHTML: "<p>abc\uD83D\uDC49</p><p>[]\uD83D\uDC48def</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("delete");
    }
  },
  {
    description: "forwarddelete at end of first paragraph ending with an emoji",
    editorInnerHTML: "<p>abc\uD83D\uDC49[]</p><p>\uD83D\uDC48def</p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("forwarddelete");
    }
  },
  {
    description: "delete at start of second paragraph ending with a non editable item",
    editorInnerHTML: "<p>A line</p><p>[]Second line with <b contenteditable='false'>non-editable item</b></p>",
    run: (win, doc, editingHost) => {
      doc.execCommand("delete");
    }
  }
];

for (const curTest of tests) {
  promise_test(async t => {
    await new Promise(resolve => {
      iframe.addEventListener("load", resolve, {once: true});
      iframe.srcdoc = "<html><body><div contenteditable></div></body></html>";
    });
    const contentDocument = iframe.contentDocument;
    const contentWindow = iframe.contentWindow;
    contentWindow.focus();
    const editingHost = contentDocument.querySelector("div[contenteditable]");
    const utils = new EditorTestUtils(editingHost, window);
    utils.setupEditingHost(curTest.editorInnerHTML);
    contentDocument.documentElement.scrollHeight;  // flush pending things
    if (typeof curTest.init == "function") {
      await curTest.init(contentWindow, contentDocument, editingHost);
    }
    const initialValue = editingHost.innerHTML;
    await curTest.run(contentWindow, contentDocument, editingHost);
    const newValue = editingHost.innerHTML;
    test(t2 => {
      const ret = contentDocument.execCommand("undo");
      if (curTest.expectedUndoResult !== undefined) {
        if (typeof curTest.expectedUndoResult == "string") {
          assert_equals(
            editingHost.innerHTML,
            curTest.expectedUndoResult,
            `${t2.name}: should restore the innerHTML value`
          );
        } else {
          assert_in_array(
            editingHost.innerHTML,
            curTest.expectedUndoResult,
            `${t2.name}: should restore one of the innerHTML values`
          );
        }
      } else {
        assert_equals(
          editingHost.innerHTML,
          initialValue,
          `${t2.name}: should restore the initial innerHTML value`
        );
      }
      assert_true(ret, `${t2.name}: execCommand("undo") should return true`);
    }, `${t.name} - first undo`);
    test(t3 => {
      const ret = contentDocument.execCommand("redo");
      assert_equals(
        editingHost.innerHTML,
        newValue,
        `${t3.name}: should restore the modified innerHTML value`
      );
      assert_true(ret, `${t3.name}: execCommand("redo") should return true`);
    }, `${curTest.description} - first redo`);
    test(t4 => {
      const ret = contentDocument.execCommand("redo");
      assert_equals(
        editingHost.innerHTML,
        newValue,
        `${t4.name}: should not modify the modified innerHTML value`
      );
      assert_false(ret, `${t4.name}: execCommand("redo") should return false`);
    }, `${curTest.description} - second redo`);
    if (typeof curTest.cleanUp == "function") {
      await curTest.cleanUp(contentWindow, contentDocument, editingHost);
    }
    await new Promise(resolve => {
      iframe.addEventListener("load", resolve, {once: true});
      iframe.srcdoc = "";
    });
    contentDocument.documentElement.scrollHeight; // flush pending things
    await new Promise(resolve =>
      requestAnimationFrame(
        () => requestAnimationFrame(resolve)
      )
    );
  }, curTest.description);
}
</script>