| <!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"); | |
| } | |
| }, | |
| ]; | |
| 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); | |
| } | |
| iframe.srcdoc = ""; | |
| contentDocument.documentElement.scrollHeight; // flush pending things | |
| await new Promise(resolve => | |
| requestAnimationFrame( | |
| () => requestAnimationFrame(resolve) | |
| ) | |
| ); | |
| }, curTest.description); | |
| } | |
| </script> |