undo-redo.html (7987B)
1 <!DOCTYPE html> 2 <script src="/resources/testharness.js"></script> 3 <script src="/resources/testharnessreport.js"></script> 4 <script src="/resources/testdriver.js"></script> 5 <script src="/resources/testdriver-vendor.js"></script> 6 <script src="/resources/testdriver-actions.js"></script> 7 <script src="../include/editor-test-utils.js"></script> 8 <iframe srcdoc=""></iframe> 9 <script> 10 "use strict"; 11 const iframe = document.querySelector("iframe"); 12 13 promise_test(async () => { 14 await new Promise(resolve => { 15 addEventListener("load", resolve, {once: true}); 16 }); 17 }, "Waiting for load..."); 18 19 /** 20 * This test does NOT test whether the edit result is valid or invalid. 21 * This test just tests whether "undo" and "redo" restores previous state 22 * and additional "undo" and "redo" does not run unexpectedly. 23 * 24 * description: Set string to explain what's testing. 25 * editorInnerHTML: Set initial innerHTML value of editor. 26 * init: Set a function object if you need to test complicated cases, e.g., 27 * testing with empty text node. 28 * run: Set a function object which run something modifying the editor (or 29 * does nothing). 30 * expectedUndoResult: Set an expected innerHTML result as string or array 31 * of the string. If this is not specified, it's compared 32 * with editorInnerHTML value. 33 * cleanUp: Set a function object if you need to clean something up after the 34 * test. 35 */ 36 37 const tests = [ 38 { 39 description: "insertParagraph at start of a paragraph", 40 editorInnerHTML: "<p>[]abcdef</p>", 41 run: (win, doc, editingHost) => { 42 doc.execCommand("insertParagraph"); 43 }, 44 }, 45 { 46 description: "insertParagraph at middle of a paragraph", 47 editorInnerHTML: "<p>abc[]def</p>", 48 run: (win, doc, editingHost) => { 49 doc.execCommand("insertParagraph"); 50 }, 51 }, 52 { 53 description: "insertParagraph at end of a paragraph", 54 editorInnerHTML: "<p>abcdef[]</p>", 55 run: (win, doc, editingHost) => { 56 doc.execCommand("insertParagraph"); 57 }, 58 }, 59 { 60 description: "insertParagraph at start of a listitem", 61 editorInnerHTML: "<ul><li>[]abcdef</li></ul>", 62 run: (win, doc, editingHost) => { 63 doc.execCommand("insertParagraph"); 64 }, 65 }, 66 { 67 description: "insertParagraph at middle of a listitem", 68 editorInnerHTML: "<ul><li>abc[]def</li></ul>", 69 run: (win, doc, editingHost) => { 70 doc.execCommand("insertParagraph"); 71 }, 72 }, 73 { 74 description: "insertParagraph at end of a listitem", 75 editorInnerHTML: "<ul><li>abcdef[]</li></ul>", 76 run: (win, doc, editingHost) => { 77 doc.execCommand("insertParagraph"); 78 }, 79 }, 80 { 81 description: "insertLineBreak at start of a paragraph", 82 editorInnerHTML: "<p>[]abcdef</p>", 83 run: (win, doc, editingHost) => { 84 doc.execCommand("insertLineBreak"); 85 }, 86 }, 87 { 88 description: "insertLineBreak at middle of a paragraph", 89 editorInnerHTML: "<p>abc[]def</p>", 90 run: (win, doc, editingHost) => { 91 doc.execCommand("insertLineBreak"); 92 }, 93 }, 94 { 95 description: "insertLineBreak at end of a paragraph", 96 editorInnerHTML: "<p>abcdef[]</p>", 97 run: (win, doc, editingHost) => { 98 doc.execCommand("insertLineBreak"); 99 }, 100 }, 101 { 102 description: "insertLineBreak at start of a listitem", 103 editorInnerHTML: "<ul><li>[]abcdef</li></ul>", 104 run: (win, doc, editingHost) => { 105 doc.execCommand("insertLineBreak"); 106 }, 107 }, 108 { 109 description: "insertLineBreak at middle of a listitem", 110 editorInnerHTML: "<ul><li>abc[]def</li></ul>", 111 run: (win, doc, editingHost) => { 112 doc.execCommand("insertLineBreak"); 113 }, 114 }, 115 { 116 description: "insertLineBreak at end of a listitem", 117 editorInnerHTML: "<ul><li>abcdef[]</li></ul>", 118 run: (win, doc, editingHost) => { 119 doc.execCommand("insertLineBreak"); 120 }, 121 }, 122 { 123 description: "delete at start of second paragraph", 124 editorInnerHTML: "<p>abc</p><p>[]def</p>", 125 run: (win, doc, editingHost) => { 126 doc.execCommand("delete"); 127 } 128 }, 129 { 130 description: "forwarddelete at end of first paragraph", 131 editorInnerHTML: "<p>abc[]</p><p>def</p>", 132 run: (win, doc, editingHost) => { 133 doc.execCommand("forwarddelete"); 134 } 135 }, 136 { 137 description: "delete at start of second paragraph starting with an emoji", 138 editorInnerHTML: "<p>abc\uD83D\uDC49</p><p>[]\uD83D\uDC48def</p>", 139 run: (win, doc, editingHost) => { 140 doc.execCommand("delete"); 141 } 142 }, 143 { 144 description: "forwarddelete at end of first paragraph ending with an emoji", 145 editorInnerHTML: "<p>abc\uD83D\uDC49[]</p><p>\uD83D\uDC48def</p>", 146 run: (win, doc, editingHost) => { 147 doc.execCommand("forwarddelete"); 148 } 149 }, 150 { 151 description: "delete at start of second paragraph ending with a non editable item", 152 editorInnerHTML: "<p>A line</p><p>[]Second line with <b contenteditable='false'>non-editable item</b></p>", 153 run: (win, doc, editingHost) => { 154 doc.execCommand("delete"); 155 } 156 } 157 ]; 158 159 for (const curTest of tests) { 160 promise_test(async t => { 161 await new Promise(resolve => { 162 iframe.addEventListener("load", resolve, {once: true}); 163 iframe.srcdoc = "<html><body><div contenteditable></div></body></html>"; 164 }); 165 const contentDocument = iframe.contentDocument; 166 const contentWindow = iframe.contentWindow; 167 contentWindow.focus(); 168 const editingHost = contentDocument.querySelector("div[contenteditable]"); 169 const utils = new EditorTestUtils(editingHost, window); 170 utils.setupEditingHost(curTest.editorInnerHTML); 171 contentDocument.documentElement.scrollHeight; // flush pending things 172 if (typeof curTest.init == "function") { 173 await curTest.init(contentWindow, contentDocument, editingHost); 174 } 175 const initialValue = editingHost.innerHTML; 176 await curTest.run(contentWindow, contentDocument, editingHost); 177 const newValue = editingHost.innerHTML; 178 test(t2 => { 179 const ret = contentDocument.execCommand("undo"); 180 if (curTest.expectedUndoResult !== undefined) { 181 if (typeof curTest.expectedUndoResult == "string") { 182 assert_equals( 183 editingHost.innerHTML, 184 curTest.expectedUndoResult, 185 `${t2.name}: should restore the innerHTML value` 186 ); 187 } else { 188 assert_in_array( 189 editingHost.innerHTML, 190 curTest.expectedUndoResult, 191 `${t2.name}: should restore one of the innerHTML values` 192 ); 193 } 194 } else { 195 assert_equals( 196 editingHost.innerHTML, 197 initialValue, 198 `${t2.name}: should restore the initial innerHTML value` 199 ); 200 } 201 assert_true(ret, `${t2.name}: execCommand("undo") should return true`); 202 }, `${t.name} - first undo`); 203 test(t3 => { 204 const ret = contentDocument.execCommand("redo"); 205 assert_equals( 206 editingHost.innerHTML, 207 newValue, 208 `${t3.name}: should restore the modified innerHTML value` 209 ); 210 assert_true(ret, `${t3.name}: execCommand("redo") should return true`); 211 }, `${curTest.description} - first redo`); 212 test(t4 => { 213 const ret = contentDocument.execCommand("redo"); 214 assert_equals( 215 editingHost.innerHTML, 216 newValue, 217 `${t4.name}: should not modify the modified innerHTML value` 218 ); 219 assert_false(ret, `${t4.name}: execCommand("redo") should return false`); 220 }, `${curTest.description} - second redo`); 221 if (typeof curTest.cleanUp == "function") { 222 await curTest.cleanUp(contentWindow, contentDocument, editingHost); 223 } 224 await new Promise(resolve => { 225 iframe.addEventListener("load", resolve, {once: true}); 226 iframe.srcdoc = ""; 227 }); 228 contentDocument.documentElement.scrollHeight; // flush pending things 229 await new Promise(resolve => 230 requestAnimationFrame( 231 () => requestAnimationFrame(resolve) 232 ) 233 ); 234 }, curTest.description); 235 } 236 </script>