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